diff --git a/package.json b/package.json index bc07b27b4b..82fa4e1689 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,12 @@ "scripts": { "init": "./scripts/init-cafe.sh", "preinstall": "node scripts/check-node-runtime.mjs", + "pregate": "node scripts/check-validation-node-runtime.mjs", "gate": "bash ./scripts/pre-merge-check.sh", + "prebuild": "node scripts/check-validation-node-runtime.mjs", + "pretest": "node scripts/check-validation-node-runtime.mjs", + "prelint": "node scripts/check-validation-node-runtime.mjs", + "precheck": "node scripts/check-validation-node-runtime.mjs", "guards:install": "bash ./scripts/install-git-guards.sh", "start": "node ./scripts/start-entry.mjs start", "stop": "./scripts/start-dev.sh --stop", @@ -19,8 +24,11 @@ "alpha:start": "./scripts/alpha-worktree.sh start", "alpha:status": "./scripts/alpha-worktree.sh status", "f210:agy-profile-smoke": "pnpm --filter @cat-cafe/api run smoke:f210-agy-profiles", + "pref210:agy-profile-smoke": "node scripts/check-validation-node-runtime.mjs", "alpha:test": "bash ./scripts/alpha-worktree.test.sh", + "prealpha:test": "node scripts/check-validation-node-runtime.mjs", "runtime:test": "bash ./scripts/runtime-worktree.test.sh", + "preruntime:test": "node scripts/check-validation-node-runtime.mjs", "develop:init": "./scripts/develop-worktree.sh init", "develop:sync": "./scripts/develop-worktree.sh sync", "develop:start": "./scripts/develop-worktree.sh start", @@ -79,11 +87,13 @@ "check:sop-definitions": "node --test scripts/sop-definitions.test.mjs && node scripts/sop-definitions.mjs --check", "check:followup-tails": "node scripts/check-followup-tails.mjs", "audit:feature-docs": "node scripts/audit-feature-doc-template.mjs", + "preaudit:feature-docs": "node scripts/check-validation-node-runtime.mjs", "check:skills": "bash scripts/check-skills-mount.sh", "check:skills:manifest": "node scripts/check-skills-manifest.mjs", "check:skills:surfaces": "node --test scripts/check-skill-first-party-surfaces.test.mjs && node scripts/check-skill-first-party-surfaces.mjs", "check:pre-merge-gate": "node --test scripts/pre-merge-check.test.mjs scripts/pre-merge-gate-guard.test.mjs scripts/test-bash-runtime.test.mjs", "check:hotfix-pattern": "node --test scripts/check-hotfix-pattern.test.mjs", + "premcp:doctor": "node scripts/check-validation-node-runtime.mjs", "mcp:doctor": "node scripts/mcp-doctor.mjs", "convention-graph": "pnpm --filter @cat-cafe/convention-graph convention-graph", "convention-graph:index": "pnpm --filter @cat-cafe/convention-graph graph:index", @@ -96,7 +106,35 @@ "clean": "pnpm -r run clean && rm -rf node_modules", "check:start-profile-isolation": "node --test scripts/start-dev-profile-isolation.test.mjs", "check:brand-dictionary": "node --test scripts/brand-dictionary-helper.test.mjs", - "check:brand-guard": "node --test scripts/intake-from-opensource.test.mjs scripts/lib/intake-gh-retry.test.mjs" + "check:brand-guard": "node --test scripts/intake-from-opensource.test.mjs scripts/lib/intake-gh-retry.test.mjs", + "precheck:biome-review-worktrees": "node scripts/check-validation-node-runtime.mjs", + "precheck:biome-version": "node scripts/check-validation-node-runtime.mjs", + "precheck:brand-dictionary": "node scripts/check-validation-node-runtime.mjs", + "precheck:brand-guard": "node scripts/check-validation-node-runtime.mjs", + "precheck:capability-tips": "node scripts/check-validation-node-runtime.mjs", + "precheck:capability-tips:stale": "node scripts/check-validation-node-runtime.mjs", + "precheck:deps": "node scripts/check-validation-node-runtime.mjs", + "precheck:dir-size": "node scripts/check-validation-node-runtime.mjs", + "precheck:env-example": "node scripts/check-validation-node-runtime.mjs", + "precheck:env-ports": "node scripts/check-validation-node-runtime.mjs", + "precheck:env-registry": "node scripts/check-validation-node-runtime.mjs", + "precheck:features": "node scripts/check-validation-node-runtime.mjs", + "precheck:fix": "node scripts/check-validation-node-runtime.mjs", + "precheck:followup-tails": "node scripts/check-validation-node-runtime.mjs", + "precheck:guides": "node scripts/check-validation-node-runtime.mjs", + "precheck:hotfix-pattern": "node scripts/check-validation-node-runtime.mjs", + "precheck:lockfile": "node scripts/check-validation-node-runtime.mjs", + "precheck:pre-merge-gate": "node scripts/check-validation-node-runtime.mjs", + "precheck:scripts-ascii-only": "node scripts/check-validation-node-runtime.mjs", + "precheck:skills": "node scripts/check-validation-node-runtime.mjs", + "precheck:skills:manifest": "node scripts/check-validation-node-runtime.mjs", + "precheck:skills:surfaces": "node scripts/check-validation-node-runtime.mjs", + "precheck:sop-definitions": "node scripts/check-validation-node-runtime.mjs", + "precheck:start-profile-isolation": "node scripts/check-validation-node-runtime.mjs", + "precheck:video-new": "node scripts/check-validation-node-runtime.mjs", + "precheck:worktree-port-offset": "node scripts/check-validation-node-runtime.mjs", + "pretest:api:redis": "node scripts/check-validation-node-runtime.mjs", + "pretest:api:redis:repeat": "node scripts/check-validation-node-runtime.mjs" }, "devDependencies": { "@biomejs/biome": "^2.4.1", diff --git a/packages/api/config/public-test-exclusions.json b/packages/api/config/public-test-exclusions.json index e19a2b5a0c..c1c057ef45 100644 --- a/packages/api/config/public-test-exclusions.json +++ b/packages/api/config/public-test-exclusions.json @@ -8,7 +8,7 @@ "reason": "Redis isolation and persistence tests are internal harness coverage, not part of the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "concurrent-fault-drill", @@ -17,7 +17,7 @@ "reason": "Fault-drill stress coverage is too heavy for the public gate and stays in internal validation.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "task-progress-store", @@ -26,7 +26,7 @@ "reason": "Task progress store behavior depends on internal persistence surfaces not exported to the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "session-strategy-phase3", @@ -35,7 +35,7 @@ "reason": "Phase 3 session strategy assertions cover internal rollout behavior outside the public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "signal-article-store", @@ -44,7 +44,7 @@ "reason": "Signal article store cases exercise source-only data plumbing not guaranteed in the public export.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "persistence-fault-drill", @@ -53,7 +53,7 @@ "reason": "Persistence fault drills are heavyweight stress scenarios reserved for internal validation.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "cursor-store-atomicity", @@ -62,7 +62,7 @@ "reason": "Atomic cursor store checks cover internal durability mechanics not enforced by the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "workflow-sop-store", @@ -71,7 +71,7 @@ "reason": "Workflow SOP store tests rely on internal governance persistence surfaces excluded from the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "codex-agent-service", @@ -80,7 +80,7 @@ "reason": "Codex agent runtime service tests depend on internal carrier/harness wiring outside the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "kimi-agent-service", @@ -89,7 +89,7 @@ "reason": "Kimi agent service coverage is internal runtime behavior, not public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "claude-settings-hooks", @@ -98,7 +98,7 @@ "reason": "Claude settings hook assertions depend on private runtime fixtures unavailable in public export.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "game-store", @@ -107,7 +107,7 @@ "reason": "Game store tests rely on non-exported local fixtures and are kept out of the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "memory-tests", @@ -116,7 +116,7 @@ "reason": "Memory package tests cover internal recall/index behavior outside the public gate promise.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "cross-cat-context", @@ -125,7 +125,7 @@ "reason": "Cross-cat context routing is internal collaboration harness behavior, not public gate surface.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "thread-wiring", @@ -134,7 +134,7 @@ "reason": "Thread wiring assertions cover internal callback plumbing outside the public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "integration-wiring", @@ -143,7 +143,7 @@ "reason": "Integration wiring checks depend on internal connection topology not exported to public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "shared-state-wiring", @@ -152,7 +152,7 @@ "reason": "Shared-state wiring is internal runtime glue not part of the public gate guarantee.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "signal-fetcher-launchd", @@ -161,7 +161,7 @@ "reason": "launchd-specific signal fetcher coverage depends on private macOS fixtures and stays internal.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "reflection-capsule-m3", @@ -170,7 +170,7 @@ "reason": "Reflection capsule M3 tests cover internal memory/harness behavior outside the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "workspace-project-context", @@ -179,7 +179,7 @@ "reason": "Workspace project context assertions depend on source-only repo structure and local project wiring.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "projects-setup", @@ -188,7 +188,7 @@ "reason": "Project setup tests cover internal bootstrap flows not guaranteed in the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "projects-mkdir", @@ -197,7 +197,7 @@ "reason": "Project directory creation coverage depends on source-only filesystem conventions.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "governance-status", @@ -206,7 +206,7 @@ "reason": "Governance status assertions are source-owned workflow coverage, not public gate behavior.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "governance-pack", @@ -215,7 +215,7 @@ "reason": "Governance pack assertions validate source-only sync content and are intentionally excluded from public gate.", "owner": "@zts212653", "introducedBy": "069d0f0fb", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "pack-integration", @@ -224,7 +224,7 @@ "reason": "Pack integration coverage exercises internal source packaging rules outside the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "project-setup-flow", @@ -233,7 +233,7 @@ "reason": "Project setup flow behavior is internal bootstrap coverage, not public gate contract.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "process-liveness-probe", @@ -242,7 +242,7 @@ "reason": "Process liveness probe timing is too contention-sensitive for the public gate.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "expedition-bootstrap", @@ -251,7 +251,7 @@ "reason": "Expedition bootstrap coverage depends on source-owned harness flows not exported publicly.", "owner": "@zts212653", "introducedBy": "78f3bc57c", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "rules-route", @@ -260,7 +260,7 @@ "reason": "Rules route assertions depend on source-only rule artifacts stripped from public export.", "owner": "@zts212653", "introducedBy": "4243948da", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "root-md-slim", @@ -269,7 +269,7 @@ "reason": "Root markdown slim tests rely on source-specific Chinese anchors not preserved in public export.", "owner": "@zts212653", "introducedBy": "7a300704a", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "audit-cc-system-prompt", @@ -278,7 +278,7 @@ "reason": "System prompt audit depends on internal L0 prompt assets not shipped in public export.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "f188-cold-start-fixtures", @@ -287,7 +287,7 @@ "reason": "F188 cold-start fixture coverage depends on internal fixture files absent from public export.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "f188-harness-consistency", @@ -296,7 +296,7 @@ "reason": "F188 harness consistency checks rely on internal fixtures and remain source-only.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "orphan-chrome-cleaner", @@ -305,7 +305,7 @@ "reason": "Orphan Chrome cleaner assertions are sensitive to local path sanitization and private fixtures.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "capabilities-route", @@ -314,7 +314,7 @@ "reason": "Managed MCP path realignment currently fails in public gate and must be tracked as a real product regression.", "owner": "@zts212653", "introducedBy": "e9bb56052", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "antigravity-run-command-executor", @@ -323,7 +323,7 @@ "reason": "Antigravity run-command executor timing is unstable on CI and remains out of the public gate until hardened.", "owner": "@zts212653", "introducedBy": "0340783c6", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "f203-phase-i-opencode-l0", @@ -332,7 +332,7 @@ "reason": "F203 opencode L0 coverage depends on internal runtime files not exported publicly.", "owner": "@zts212653", "introducedBy": "9c4f26fde", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "f236-cc-anchor-hook", @@ -341,7 +341,7 @@ "reason": "F236 cc anchor hook coverage imports the source-only root .claude hook implementation that is not part of the public export.", "owner": "@zts212653", "introducedBy": "68dd499d9", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "github-schedule-factories", @@ -350,7 +350,7 @@ "reason": "GitHub schedule factory assertions are source-owned public-sync governance coverage, not runtime gate behavior.", "owner": "@zts212653", "introducedBy": "bd8823b99", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "harness-eval-hub-read-model", @@ -359,7 +359,7 @@ "reason": "Eval hub read-model coverage exercises source-owned harness governance surfaces outside the public gate.", "owner": "@zts212653", "introducedBy": "bd8823b99", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" }, { "id": "harness-eval-merge-gate-provenance-contract", @@ -368,7 +368,7 @@ "reason": "Merge-gate provenance contract tests are source-owned governance coverage, not public runtime behavior.", "owner": "@zts212653", "introducedBy": "bd8823b99", - "expiresOn": "2026-07-31" + "expiresOn": "2026-09-30" } ] } diff --git a/packages/api/package.json b/packages/api/package.json index 2a37cbc684..31351e7b36 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -7,12 +7,16 @@ "scripts": { "dev": "while true; do NODE_OPTIONS=\"--import $PWD/scripts/sigusr1-guard.mjs${NODE_OPTIONS:+ $NODE_OPTIONS}\" tsx watch --exclude \"dist/**\" --exclude \"../shared/dist/**\" src/index.ts; ec=$?; if [ $ec -eq 0 ] || [ $ec -eq 130 ]; then break; fi; echo \"[api] dev exited ($ec) — auto-restarting in 1s\"; sleep 1; done", "verify:sigusr1": "node scripts/verify-sigusr1-guard.mjs", + "preverify:sigusr1": "node ../../scripts/check-validation-node-runtime.mjs", + "prebuild": "node ../../scripts/check-validation-node-runtime.mjs", "build": "pnpm --dir ../shared build && tsc && node ./scripts/copy-marketplace-catalog-data.mjs", "start": "node dist/index.js", + "pretest": "node ../../scripts/check-validation-node-runtime.mjs", "test": "pnpm --filter @cat-cafe/mcp-server... build && pnpm run build && CAT_CAFE_DISABLE_SHARED_STATE_PREFLIGHT=1 bash ./scripts/with-test-home.sh node --import $(pwd)/test/helpers/setup-cat-registry.js --test --test-timeout=60000 test/*.test.js test/**/*.test.js && pnpm run test:cli", "test:public": "pnpm --dir ../shared build && pnpm --dir ../mcp-server build && pnpm run build && CAT_CAFE_DISABLE_SHARED_STATE_PREFLIGHT=1 bash ./scripts/with-test-home.sh bash ./scripts/run-public-tests.sh", "test:antigravity-smoke": "RUN_ANTIGRAVITY_SMOKE=true pnpm run build && RUN_ANTIGRAVITY_SMOKE=true bash ./scripts/with-test-home.sh node --test test/antigravity-smoke.test.js", "smoke:f210-agy-profiles": "pnpm run build && node dist/scripts/f210-agy-profile-smoke.js", + "presmoke:f210-agy-profiles": "node ../../scripts/check-validation-node-runtime.mjs", "fetch-signals": "node dist/scripts/fetch-signals.js", "migrate-signals": "node dist/scripts/migrate-signals.js", "rebuild-index": "node dist/scripts/rebuild-index.js", @@ -22,8 +26,16 @@ "test:integration": "bash ./scripts/with-test-home.sh node --test test/integration/*.test.js", "test:pty": "pnpm run build && node --import $(pwd)/test/helpers/setup-cat-registry.js --test --test-timeout=120000 test/f230-pty-driver.test.js", "test:cli": "node --import tsx --test test/cli/*.test.ts", + "prelint": "node ../../scripts/check-validation-node-runtime.mjs", "lint": "tsc --noEmit", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "pretest:antigravity-smoke": "node ../../scripts/check-validation-node-runtime.mjs", + "pretest:cli": "node ../../scripts/check-validation-node-runtime.mjs", + "pretest:integration": "node ../../scripts/check-validation-node-runtime.mjs", + "pretest:pty": "node ../../scripts/check-validation-node-runtime.mjs", + "pretest:public": "node ../../scripts/check-validation-node-runtime.mjs", + "pretest:redis": "node ../../scripts/check-validation-node-runtime.mjs", + "pretest:redis:repeat": "node ../../scripts/check-validation-node-runtime.mjs" }, "dependencies": { "@cat-cafe/shared": "workspace:*", diff --git a/packages/api/src/config/env-registry.ts b/packages/api/src/config/env-registry.ts index 6098fb19ce..24b24e57a2 100644 --- a/packages/api/src/config/env-registry.ts +++ b/packages/api/src/config/env-registry.ts @@ -198,6 +198,62 @@ export const ENV_VARS: EnvDefinition[] = [ sensitive: false, runtimeEditable: false, }, + { + name: 'CAT_CAFE_NODE_MIN_MAJOR', + defaultValue: '24', + description: 'Node runtime guard 最低允许主版本(安装/验证脚本启动时读取)', + category: 'server', + sensitive: false, + runtimeEditable: false, + }, + { + name: 'CAT_CAFE_NODE_MAX_MAJOR_EXCLUSIVE', + defaultValue: '26', + description: 'Node runtime guard 排他的最高主版本边界(安装/验证脚本启动时读取)', + category: 'server', + sensitive: false, + runtimeEditable: false, + }, + { + name: 'CAT_CAFE_NODE_PINNED_MAJOR', + defaultValue: '24', + description: '启动脚本优先 re-exec 到的 Node 主版本;用于避免 Homebrew/node alias 自动漂移', + category: 'server', + sensitive: false, + runtimeEditable: false, + }, + { + name: 'CAT_CAFE_NODE_PREFERRED_MAJORS', + defaultValue: '24 25', + description: '启动脚本搜索本机 Node runtime 的主版本候选顺序(空格分隔)', + category: 'server', + sensitive: false, + runtimeEditable: false, + }, + { + name: 'CAT_CAFE_NODE_BIN', + defaultValue: '(未设置)', + description: '启动脚本优先使用的 Node 可执行文件绝对路径;用于多 Node 环境或手动指定受支持 runtime', + category: 'server', + sensitive: false, + runtimeEditable: false, + }, + { + name: 'CAT_CAFE_SKIP_NODE_RUNTIME_GUARD', + defaultValue: '0', + description: '跳过 Node runtime guard 的安装期应急开关;仅在明确知道风险时使用', + category: 'server', + sensitive: false, + runtimeEditable: false, + }, + { + name: 'CAT_CAFE_SKIP_PRODUCTION_INSTALL_GUARD', + defaultValue: '0', + description: '仅跳过 production install guard,保留 Node 版本检查;供验证脚本绕过安装模式检查使用', + category: 'server', + sensitive: false, + runtimeEditable: false, + }, { name: 'CAT_CAFE_INVOCATION_REGISTRY', defaultValue: '(自动:有 Redis 用 redis,否则 memory)', diff --git a/packages/api/test/public-test-exclusions.test.js b/packages/api/test/public-test-exclusions.test.js index df55e171e8..9e42bff7ed 100644 --- a/packages/api/test/public-test-exclusions.test.js +++ b/packages/api/test/public-test-exclusions.test.js @@ -98,7 +98,7 @@ test('registry preserves metadata for active legacy exclusions and drops stale o category: 'source_only', owner: '@zts212653', introducedBy: '069d0f0fb', - expiresOn: '2026-07-31', + expiresOn: '2026-09-30', }, ); }); diff --git a/packages/finance/package.json b/packages/finance/package.json index e94418aca0..bdf9d0fd61 100644 --- a/packages/finance/package.json +++ b/packages/finance/package.json @@ -13,10 +13,14 @@ }, "scripts": { "build": "tsc", + "preprepare": "node ../../scripts/check-validation-node-runtime.mjs", "prepare": "tsc", "lint": "tsc --noEmit", "test": "pnpm run build && node --test test/*.test.js", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "prebuild": "node ../../scripts/check-validation-node-runtime.mjs", + "prelint": "node ../../scripts/check-validation-node-runtime.mjs", + "pretest": "node ../../scripts/check-validation-node-runtime.mjs" }, "devDependencies": { "typescript": "^5.3.3" diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index fd061802f4..cbad6ee9a1 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -5,11 +5,14 @@ "type": "module", "main": "./dist/index.js", "scripts": { + "prebuild": "node ../../scripts/check-validation-node-runtime.mjs", "build": "tsc", "dev": "tsx watch src/index.ts", "start": "node dist/index.js", "clean": "rm -rf dist", + "prelint": "node ../../scripts/check-validation-node-runtime.mjs", "lint": "tsc --noEmit", + "pretest": "node ../../scripts/check-validation-node-runtime.mjs", "test": "tsc && node --import tsx --test test/*.test.js test/*.test.ts" }, "dependencies": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 485c357008..704ec0423e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -32,11 +32,15 @@ } }, "scripts": { + "prebuild": "node ../../scripts/check-validation-node-runtime.mjs", "build": "tsc", + "preprepare": "node ../../scripts/check-validation-node-runtime.mjs", "prepare": "tsc", "dev": "tsc --watch", "clean": "rm -rf dist", + "prelint": "node ../../scripts/check-validation-node-runtime.mjs", "lint": "tsc --noEmit", + "pretest": "node ../../scripts/check-validation-node-runtime.mjs", "test": "vitest run" }, "dependencies": { diff --git a/packages/web/package.json b/packages/web/package.json index d202a3eeca..584934e053 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -6,13 +6,16 @@ "sync:vendor-assets": "node scripts/sync-vendor-assets.mjs", "predev": "pnpm run sync:vendor-assets", "dev": "node scripts/sync-vendor-assets.mjs --watch -- next dev", - "prebuild": "pnpm run sync:vendor-assets", + "prebuild": "node ../../scripts/check-validation-node-runtime.mjs && pnpm run sync:vendor-assets", "build": "next build", "prestart": "pnpm run sync:vendor-assets", "start": "next start", + "prelint": "node ../../scripts/check-validation-node-runtime.mjs", "lint": "next lint", + "pretest": "node ../../scripts/check-validation-node-runtime.mjs", "test": "node scripts/run-with-node-env-test.mjs pnpm exec vitest run && node scripts/run-with-node-env-test.mjs node --test test/next-config.test.cjs && node eslint-plugins/no-hardcoded-colors.test.js", - "test:lint-rules": "node eslint-plugins/no-hardcoded-colors.test.js" + "test:lint-rules": "node eslint-plugins/no-hardcoded-colors.test.js", + "pretest:lint-rules": "node ../../scripts/check-validation-node-runtime.mjs" }, "dependencies": { "@cat-cafe/shared": "workspace:*", diff --git a/review-notes/2026-08-02-node24-runtime-guard-review-request.md b/review-notes/2026-08-02-node24-runtime-guard-review-request.md new file mode 100644 index 0000000000..d27550c5ec --- /dev/null +++ b/review-notes/2026-08-02-node24-runtime-guard-review-request.md @@ -0,0 +1,121 @@ +# Review Request: Node 24 Runtime Guard for Direct Validation Scripts + +Review-Target-ID: fix-node24-runtime-guard +Branch: fix/node24-runtime-guard +Commit: de99ff6c9b3c6297c3a08a0149749e68d2a89bad +Worktree: `/Users/xxx/workspace/AI/cat-cafe-node24-runtime-guard` + +## What + +Added a cross-platform validation wrapper, `scripts/check-validation-node-runtime.mjs`, and wired it into root/package `prebuild`, `pretest`, `prelint`, plus root `precheck`/`pregate`. + +The existing install-time guard still rejects `NODE_ENV=production` for installs. Direct validation scripts now skip only that install-specific check while keeping the Node major check, so unsupported Node versions fail before running package work. + +## Why + +The 2026-08-02 scheduled patrol reproduced a false-negative path: `pnpm --filter @cat-cafe/mcp-server test ...` under default Node v22.22.3 ran part of the suite and ended with cancelled tests, while the same command under Node v24.18.0 passed. The repo already declares `engines.node >=24`, but pnpm only warned during direct package scripts. + +## Original Requirements + +> 每轮必须先查真相源和证据,再给风险/价值判断与下一步动作。发现可执行事项后主导闭环:按家规走 feature lifecycle(定位真相源、立项、实现/协调、质量门禁、review、完成记录)。 + +- 来源:scheduled patrol dispatch in `thread_mqcj45byxoka2z7u`, 2026-08-02 00:00 Asia/Shanghai +- Task: `0001785600245033-001028-850d142c` (`[巡检] 固化 Cat Cafe 验证命令的 Node 24 运行时`) +- Please judge whether this closes the patrol finding without over-expanding runtime/tooling scope. + +## Tradeoff + +I did not add a new dependency such as `cross-env`, and I did not change install semantics. A small Node wrapper avoids POSIX-only `VAR=1 node ...` scripts and keeps Windows package-script compatibility. + +This is fail-fast, not auto-reexec, for package validation scripts. Startup scripts already have reexec behavior through `scripts/lib/node-runtime-guard.sh`; package scripts should be deterministic and explicit when the shell runtime is wrong. + +## Architecture Ownership + +Architecture cell: harness-eval +Map delta: none +Why: This only extends validation/preflight harness behavior around existing package scripts and the existing Node runtime guard. It does not add a new Store, Queue, Router, Adapter, Dispatcher, Binding, service boundary, or runtime ownership cell. + +Please reviewer-check that `Map delta: none` matches the diff and that package lifecycle hooks do not create a parallel tooling/runtime control plane. + +## Open Questions + +### 技术 OQ(给 reviewer) + +1. Is `CAT_CAFE_SKIP_PRODUCTION_INSTALL_GUARD` scoped narrowly enough, or should the wrapper use a more validation-specific API to avoid future misuse? +2. Are the selected lifecycle hooks sufficient: root `build/test/lint/check/gate` and package `build/test/lint` for api/mcp/shared/web? +3. Any concern that package `pre*` hooks create surprising behavior for downstream/open-source users? + +### 价值 OQ(给 operator,如有) + +无。 + +## Next Action + +Please review commit `de99ff6c9b3c6297c3a08a0149749e68d2a89bad` in `/Users/xxx/workspace/AI/cat-cafe-node24-runtime-guard`. + +Verdict requested: APPROVE or REQUEST-CHANGES. No GitHub PR exists yet; if approved, I will open/push the PR or continue merge-gate according to the current house flow. + +## Review Sandbox + +- Path: `/tmp/cat-cafe-review/fix-node24-runtime-guard/sol` (or reviewer handle) +- Start Command: not needed; no runtime/server changes +- Ports: not applicable + +### Sandbox Bootstrap + +```bash +unset NODE_ENV +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm install --frozen-lockfile +``` + +## Self-Check Evidence + +### Spec 合规 + +- Patrol finding reproduced under default Node v22.22.3. +- Red test added first in `scripts/node-runtime-guard.test.mjs`; it failed on missing lifecycle guards. +- Implementation adds validation wrapper and lifecycle hooks. +- Dogfood verified default Node v22 now fails fast before test execution, while Node v24 executes the focused mcp suite. +- No frontend/runtime server change; no browser evidence required. +- Root artifact hygiene: both root media checks returned no matches. + +### Test Results + +```bash +PATH="/opt/homebrew/opt/node@24/bin:$PATH" node --test scripts/node-runtime-guard.test.mjs +# 14 pass / 0 fail / 0 cancelled + +pnpm --filter @cat-cafe/mcp-server test -- --test-name-pattern "workflow-mandated|cat_cafe_register_scheduled_task" +# Expected fail-fast on Node v22.22.3: +# [node-runtime] Node 22.22.3 is not supported... + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/mcp-server test -- --test-name-pattern "workflow-mandated|cat_cafe_register_scheduled_task" +# 381 pass / 0 fail / 0 cancelled + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm check +# PASS; advisory warnings only from existing capability-tip / skill manifest checks + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm -r --workspace-concurrency=1 --if-present run prebuild +# PASS; web vendor asset sync preserved + +git diff --check +# PASS +``` + +### Root Artifact Hygiene + +```bash +git status --short | rg '^.. [^/]+\.(png|jpe?g|webp|gif|webm|mp4|mov|wav|pdf|pen)$' +# no output + +git diff --name-only origin/develop...HEAD | rg '^[^/]+\.(png|jpe?g|webp|gif|webm|mp4|mov|wav|pdf|pen)$' +# no output +``` + +### Related Evidence + +- Existing root guard: `scripts/check-node-runtime.mjs` +- New validation wrapper: `scripts/check-validation-node-runtime.mjs` +- Regression coverage: `scripts/node-runtime-guard.test.mjs` + +[砚砚/gpt-5.5🐾] diff --git a/review-notes/2026-08-03-node24-runtime-guard-r2-review-request.md b/review-notes/2026-08-03-node24-runtime-guard-r2-review-request.md new file mode 100644 index 0000000000..5ca44b8657 --- /dev/null +++ b/review-notes/2026-08-03-node24-runtime-guard-r2-review-request.md @@ -0,0 +1,84 @@ +# Review Request R2: Node 24 Runtime Guard Coverage + +Review-Target-ID: fix-node24-runtime-guard +Branch: fix/node24-runtime-guard +Base: origin/develop +Code Commit: 82078353b354bf8a779fe2d60f45c9a81bd2b902 +Worktree: `/Users/xxx/workspace/AI/cat-cafe-node24-runtime-guard` + +## What + +R1 reviewer finding from @sol was correct: package lifecycle hooks only protect exact script names. The first implementation protected canonical `build/test/lint` for a hardcoded package list, but left supported commands such as `packages/finance lint`, API `test:public`, and web `test:lint-rules` able to run under default Node v22. + +R2 adds a central pnpm guard: + +- `.npmrc`: `engine-strict=true` +- `scripts/node-runtime-guard.test.mjs`: replaces fixed package/script assertions with workspace package discovery plus validation entrypoint discovery (`build`, `test`, `lint`, `check`, `gate`, `test:*`, `check:*`) + +The invariant is now: either a validation entrypoint has its exact `pre` lifecycle guard, or the repo has central pnpm engine-strict protection before any package work runs. + +## Why + +This closes the original failure mode without growing a brittle list of `pretest:*` and `precheck:*` hooks. Future workspace packages or namespaced validation commands are discovered by the regression test. + +## Tradeoff + +`engine-strict=true` changes default pnpm behavior under unsupported Node from a warning to a hard failure. That is intentional for this repo because direct validation output under the wrong Node is not trustworthy. Existing Node 24 paths continue to run. + +## Failure-Mode Sweep + +Pattern: exact lifecycle hook inventory was used as the guard boundary. + +Sweep result: + +- Root `check:*` and `test:*` entrypoints were unprotected. +- API `test:*` entrypoints including `test:public` were unprotected. +- `packages/finance` canonical `build/test/lint` was unprotected. +- Web `test:lint-rules` was unprotected. + +Fix shape: central engine-strict plus dynamic test, not per-entry patching. + +## Validation Evidence + +```bash +PATH="/opt/homebrew/opt/node@24/bin:$PATH" node --test scripts/node-runtime-guard.test.mjs +# 14 pass / 0 fail / 0 cancelled + +pnpm --filter @cat-cafe/finance run lint +# default Node v22.22.3: ERR_PNPM_UNSUPPORTED_ENGINE before tsc + +pnpm --filter @cat-cafe/api run test:public +# default Node v22.22.3: ERR_PNPM_UNSUPPORTED_ENGINE before package script + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/finance run lint +# PASS + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/web run test:lint-rules +# PASS + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/mcp-server test -- --test-name-pattern "workflow-mandated|cat_cafe_register_scheduled_task" +# 381 pass / 0 fail / 0 cancelled + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm check +# PASS; existing advisory warnings only + +git diff --check +# PASS +``` + +Note: under Node 24, `pnpm --filter @cat-cafe/api run test:public` reaches the public-test resolver but fails because `packages/api/config/public-test-exclusions.json` entries expired on 2026-07-31. That is a separate existing gate-health issue, tracked as task `0001785687002134-001212-84b1606d`. + +## Quality-Gate Notes + +- Artifact hygiene: no root media/design artifacts in worktree or branch diff. +- Fallback layer script: unavailable in this checkout (`scripts/check-fallback-layers.mjs` missing); manual diff scan found no >=3 fallback-layer additions. +- Architecture ownership script: unavailable in this checkout (`check:architecture-ownership` intentionally absent from public package scripts). +- Architecture cell: harness-eval +- Map delta: none +- Why: validation/preflight behavior only; no Store, Queue, Router, Adapter, Dispatcher, Binding, or service boundary change. + +## Next Action + +Please re-review current branch HEAD after `82078353b` and give APPROVE or REQUEST-CHANGES. If approved, I will continue PR/merge-gate flow. + +[砚砚/gpt-5.5🐾] diff --git a/review-notes/2026-08-03-node24-runtime-guard-r3-review-request.md b/review-notes/2026-08-03-node24-runtime-guard-r3-review-request.md new file mode 100644 index 0000000000..ba309d8700 --- /dev/null +++ b/review-notes/2026-08-03-node24-runtime-guard-r3-review-request.md @@ -0,0 +1,105 @@ +# Review Request R3: Node 24 Runtime Guard Coverage + +Review-Target-ID: fix-node24-runtime-guard +Branch: fix/node24-runtime-guard +Base: origin/develop +Code Commit: 69a2e402af0b96f5e2def45bfc5dedb2ca942257 +Worktree: `/Users/xxx/workspace/AI/cat-cafe-node24-runtime-guard` +Reviewer: `@sol` + +## What + +R3 addresses Sol R2 findings against `4e37805077826cf1299bfff4486ff0cafb969532`. + +- P1 fixed: removed global `.npmrc` `engine-strict=true`, so startup and operational scripts can still reach `scripts/start-entry.mjs` and its Node 24 auto-reexec guard under default Node 22. +- P2 fixed: `scripts/node-runtime-guard.test.mjs` now uses POSIX path helpers for workspace package discovery and guard path construction, matching package.json script text on Windows and Unix. +- Coverage kept: validation entrypoints are still discovered dynamically and must have exact `pre` guards. The test no longer accepts central `engine-strict` as a substitute. + +## Why + +The original patrol finding is still valid: validation output from this checkout is not trustworthy under Node 22 because the repo requires Node >=24. R2's central `engine-strict` approach closed that gap, but it expanded the blast radius into startup recovery. R3 narrows enforcement back to validation entrypoints while preserving startup semantics. + +## Tradeoff + +This is more verbose than a global pnpm setting because package-level `test:*` / `check:*` commands need explicit prehooks. The dynamic audit is the guard against future drift: new validation entrypoints fail `node-runtime-guard.test.mjs` until their matching lifecycle hook is added. + +## Failure-Mode Sweep + +Pattern: a broad package-manager guard fixed validation drift but crossed a runtime boundary. + +Sweep result: + +- Startup and operational scripts are explicitly classified as non-validation entrypoints in the regression test: `start`, `start:status`, `start:direct`, `dev:direct`. +- Root `check:*` and Redis test wrappers now have explicit guards. +- API `test:*`, finance `build/lint/test`, and web `test:lint-rules` now have explicit guards. +- Path comparison code now stays in POSIX form when comparing against package.json scripts. + +## Validation Evidence + +```bash +node -v && pnpm -v +# v22.22.3 +# 9.15.4 + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" node --test scripts/node-runtime-guard.test.mjs +# 15 pass / 0 fail / 0 cancelled + +pnpm --filter @cat-cafe/finance run lint +# default Node v22.22.3: fails in @cat-cafe/finance prelint via check-validation-node-runtime.mjs before tsc + +pnpm --filter @cat-cafe/api run test:public +# default Node v22.22.3: fails in @cat-cafe/api pretest:public via check-validation-node-runtime.mjs before package work + +pnpm check:features +# default Node v22.22.3: fails in root precheck:features via check-validation-node-runtime.mjs + +env -u CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC \ + CAT_CAFE_NODE_BIN=/opt/homebrew/opt/node@24/bin/node \ + pnpm start:status +# reaches node ./scripts/start-entry.mjs status +# re-execs with /opt/homebrew/opt/node@24/bin/node (24.18.0) +# exits 1 only because the local daemon is not running; no ERR_PNPM_UNSUPPORTED_ENGINE + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/finance run lint +# PASS + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/web run test:lint-rules +# PASS + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/mcp-server test -- --test-name-pattern "workflow-mandated|cat_cafe_register_scheduled_task" +# 381 pass / 0 fail / 0 cancelled + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm check +# PASS; existing advisory warnings only + +git diff --check +# PASS +``` + +Known unrelated gate-health issue: + +```bash +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/api run test:public +# Reaches real public-test resolver under Node 24, then fails because +# packages/api/config/public-test-exclusions.json has expired "redis" exclusion: +# 2026-07-31 < 2026-08-02 +``` + +That is tracked separately as task `0001785687002134-001212-84b1606d`. + +## Quality-Gate Notes + +- Artifact hygiene: no root media/design artifacts in worktree or branch diff. +- Design check: no `designs/` directory in this checkout. +- Dogfood scope: internal validation/startup guard bugfix; startup status path was manually exercised as the user-visible recovery path. +- Fallback layer script: unavailable in this checkout (`scripts/check-fallback-layers.mjs` missing); diff adds no fallback stack. +- Architecture ownership script: unavailable in this checkout (`check:architecture-ownership` missing). +- Architecture cell: harness-eval +- Map delta: none +- Why: validation/preflight behavior only; no Store, Queue, Router, Adapter, Dispatcher, Binding, or service boundary change. + +## Next Action + +Please re-review `69a2e402af0b96f5e2def45bfc5dedb2ca942257` and the R3 packet, then give APPROVE or REQUEST-CHANGES. If approved, I will continue PR/merge-gate flow. + +[砚砚/gpt-5.5🐾] diff --git a/review-notes/2026-08-03-node24-runtime-guard-r4-review-request.md b/review-notes/2026-08-03-node24-runtime-guard-r4-review-request.md new file mode 100644 index 0000000000..9b2fc4e32b --- /dev/null +++ b/review-notes/2026-08-03-node24-runtime-guard-r4-review-request.md @@ -0,0 +1,128 @@ +# Review Request R4: Node 24 Runtime Guard Coverage + +Review-Target-ID: fix-node24-runtime-guard +Branch: fix/node24-runtime-guard +Base: origin/develop +Code Commit: d5d174773526c95d291d053e0685564f98158175 +Worktree: `/Users/xxx/workspace/AI/cat-cafe-node24-runtime-guard` +Reviewer: `@sol` + +## What + +R4 addresses Sol R3 finding against `f2b0d6bf48d4ed5f5d17d005e8daeda4f91909bb`. + +- `isValidationEntrypoint()` now recognizes validation intent by colon-delimited tokens: `audit`, `build`, `check`, `gate`, `lint`, `smoke`, `test`, `verify`, plus suffix `*-smoke`. +- Added exact prehooks for every existing R3 sweep miss: + - `package.json#f210:agy-profile-smoke` + - `package.json#alpha:test` + - `package.json#runtime:test` + - `package.json#audit:feature-docs` + - `packages/api/package.json#verify:sigusr1` + - `packages/api/package.json#smoke:f210-agy-profiles` +- Added an explicit regression test that keeps `verify:sigusr1`, `audit:*`, `smoke:*`, `*:test`, and `*-smoke` in the validation-entrypoint set while keeping startup scripts out. + +## Why + +R3 showed the previous dynamic audit still depended on a narrow name whitelist. `verify:sigusr1` could run under default Node 22 and print a trustworthy-looking `PASS`, which violates the patrol objective: validation output from this checkout must not be produced under unsupported Node. + +## Tradeoff + +This stays with scoped lifecycle hooks. I did not restore global `.npmrc engine-strict=true`, so startup/operational commands retain their Node 24 auto-reexec path. + +## Red -> Green + +Red step: + +```bash +PATH="/opt/homebrew/opt/node@24/bin:$PATH" node --test scripts/node-runtime-guard.test.mjs +# FAIL: validation entrypoints without node runtime guard: +# package.json#alpha:test +# package.json#audit:feature-docs +# package.json#f210:agy-profile-smoke +# package.json#runtime:test +# packages/api/package.json#smoke:f210-agy-profiles +# packages/api/package.json#verify:sigusr1 +``` + +Green step: + +```bash +PATH="/opt/homebrew/opt/node@24/bin:$PATH" node --test scripts/node-runtime-guard.test.mjs +# 16 pass / 0 fail / 0 cancelled +``` + +## Failure-Mode Sweep + +Pattern: validation semantics were inferred from an incomplete exact/prefix list. + +Sweep result: + +- `verify:*`: protected (`verify:sigusr1`) +- `audit:*`: protected (`audit:feature-docs`) +- `smoke:*`: protected (`smoke:f210-agy-profiles`) +- `*-smoke`: protected (`f210:agy-profile-smoke`) +- `*:test`: protected (`alpha:test`, `runtime:test`) +- startup/operational scripts remain excluded (`start`, `start:status`, `start:direct`, `dev:direct`) + +## Validation Evidence + +Default Node 22 fail-fast: + +```bash +pnpm --filter @cat-cafe/api run verify:sigusr1 +# fails in preverify:sigusr1 via check-validation-node-runtime.mjs before printing PASS + +pnpm --filter @cat-cafe/api run smoke:f210-agy-profiles +# fails in presmoke:f210-agy-profiles via check-validation-node-runtime.mjs + +pnpm run f210:agy-profile-smoke +# fails in pref210:agy-profile-smoke via check-validation-node-runtime.mjs + +pnpm run alpha:test +# fails in prealpha:test via check-validation-node-runtime.mjs + +pnpm run runtime:test +# fails in preruntime:test via check-validation-node-runtime.mjs + +pnpm run audit:feature-docs +# fails in preaudit:feature-docs via check-validation-node-runtime.mjs +``` + +Node 24 positive paths and regression gates: + +```bash +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm --filter @cat-cafe/api run verify:sigusr1 +# PASS — guard suppresses SIGUSR1 inspector across the tsx watch tree + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm run audit:feature-docs +# PASS: docs=256 green=240 yellow=16 red=0 +# generated audit artifacts were cleaned and are not part of this diff + +env -u CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC \ + CAT_CAFE_NODE_BIN=/opt/homebrew/opt/node@24/bin/node \ + pnpm start:status +# reaches start-entry.mjs, re-execs Node 24, exits only because daemon is not running + +PATH="/opt/homebrew/opt/node@24/bin:$PATH" pnpm check +# PASS; existing advisory warnings only + +git diff --check +# PASS +``` + +## Quality-Gate Notes + +- Artifact hygiene: no root media/design artifacts in worktree or branch diff. +- Design check: no `designs/` directory in this checkout. +- Dogfood scope: internal validation/startup guard bugfix; exercised actual validation commands plus startup status recovery. +- Fallback layer script: unavailable in this checkout (`scripts/check-fallback-layers.mjs` missing); diff adds no fallback stack. +- Architecture ownership script: unavailable in this checkout (`check:architecture-ownership` missing). +- Architecture cell: harness-eval +- Map delta: none +- Why: validation/preflight behavior only; no Store, Queue, Router, Adapter, Dispatcher, Binding, or service boundary change. + +## Next Action + +Please re-review `d5d174773526c95d291d053e0685564f98158175` and the R4 packet, then give APPROVE or REQUEST-CHANGES. If approved, I will continue PR/merge-gate flow. + +[砚砚/gpt-5.5🐾] diff --git a/review-notes/2026-08-09-node24-runtime-guard-r5-review-request.md b/review-notes/2026-08-09-node24-runtime-guard-r5-review-request.md new file mode 100644 index 0000000000..4bc63c8466 --- /dev/null +++ b/review-notes/2026-08-09-node24-runtime-guard-r5-review-request.md @@ -0,0 +1,79 @@ +# Review Request R5: Node 24 Runtime Guard Env Registry Closure + +Review-Target-ID: fix-node24-runtime-guard +Branch: fix/node24-runtime-guard +Base: origin/develop +Code Commit: 8e570abbc4ad883be6c84108d25132c33617c3b3 +Worktree: `/Users/xxx/workspace/AI/cat-cafe-node24-runtime-guard` +Reviewer: `@sol` + +## What + +Daily patrol found that R4's env-registry closure only scanned JS runtime guard scripts. The actual startup re-exec guard in `scripts/lib/node-runtime-guard.sh` still had undiscovered `CAT_CAFE_NODE_*` configuration, including the documented `CAT_CAFE_NODE_BIN` recovery knob. + +R5 closes that gap: + +- `scripts/check-env-registry.test.mjs` now scans `scripts/lib/node-runtime-guard.sh` for shell `$ENV_VAR` / `${ENV_VAR:-default}` references. +- `CAT_CAFE_NODE_PINNED_MAJOR`, `CAT_CAFE_NODE_PREFERRED_MAJORS`, and `CAT_CAFE_NODE_BIN` are registered in `packages/api/src/config/env-registry.ts`. +- Internal sentinel `CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC` is allowlisted with an explicit reason instead of being surfaced as user config. + +## Why + +The runtime guard tells operators to set `CAT_CAFE_NODE_BIN=/absolute/path/to/node`, but the registry did not expose that variable and the completeness test could not catch shell guard drift. That made the previous "register runtime guard env vars" fix incomplete. + +## Tradeoff + +This keeps scanning scoped to the Node runtime guard shell script instead of broadly parsing every shell script in the repo. Broad shell parsing is useful later, but not necessary for this bug and likely to produce noisy false positives. + +## Red -> Green + +Red step: + +```bash +node --test scripts/check-env-registry.test.mjs +# FAIL: 4 env var(s) used in code but not registered in env-registry.ts: +# CAT_CAFE_NODE_PINNED_MAJOR +# CAT_CAFE_NODE_PREFERRED_MAJORS +# CAT_CAFE_NODE_BIN +# CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC +``` + +Green steps: + +```bash +node --test scripts/check-env-registry.test.mjs +# pass 6 / fail 0 + +node --test scripts/node-runtime-guard.test.mjs +# pass 16 / fail 0 + +git diff --check +# PASS + +pnpm biome check packages/api/src/config/env-registry.ts scripts/check-env-registry.test.mjs --diagnostic-level=error +# Checked 2 files. No fixes applied. +# Note: pnpm emitted the expected Node engine warning because this shell is Node v22.22.3. +``` + +## Quality Gate + +Spec / intent source: R4 review packet + patrol finding against current `scripts/lib/node-runtime-guard.sh`. + +- Vision coverage: operator/cat-visible startup recovery knobs must be discoverable; `CAT_CAFE_NODE_BIN` is now registered. +- Design check: no UI/design changes; no `.pen` applicable. +- Dogfood: internal guard bugfix; exercised the actual registry gate and runtime guard tests. +- Artifact hygiene: no root media/design artifacts added. +- Architecture ownership: no cell delta; env registry + test gate only. +- Fallback layer check: no fallback stack added. + +## Review Focus + +- Confirm shell env extraction is appropriately scoped and not too broad. +- Confirm `CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC` belongs in allowlist, while `CAT_CAFE_NODE_BIN` / pinned / preferred majors belong in registry. +- Confirm R5 supersedes stale R4 `Code Commit: d5d1747...`; current code target is `8e570abbc4ad883be6c84108d25132c33617c3b3`. + +## Next Action + +Please review `8e570abbc4ad883be6c84108d25132c33617c3b3` and this R5 packet. If approved, I will continue PR/merge-gate flow. + +[砚砚/gpt-5.5🐾] diff --git a/scripts/check-env-registry.test.mjs b/scripts/check-env-registry.test.mjs index 7c7c018323..de58450863 100644 --- a/scripts/check-env-registry.test.mjs +++ b/scripts/check-env-registry.test.mjs @@ -1,8 +1,8 @@ /** * check:env-registry — CI gate for env var registration completeness. * - * Scans `packages/api/src` and `packages/mcp-server/src` for `process.env.XXX` - * references and verifies each is either: + * Scans `packages/api/src`, `packages/mcp-server/src`, and runtime guard scripts + * for `process.env.XXX` / shell `$ENV_VAR` references and verifies each is either: * 1. Registered in `env-registry.ts` ENV_VARS array, OR * 2. Listed in the ALLOWLIST below (with a reason). * @@ -39,8 +39,12 @@ const ALLOWLIST = new Map([ ['all_proxy', 'Standard proxy convention (lowercase variant of ALL_PROXY)'], ['npm_execpath', 'Package-manager metadata injected by npm/pnpm; not user-configurable'], ['npm_config_user_agent', 'Package-manager metadata injected by npm/pnpm; not user-configurable'], + ['npm_config_production', 'Package-manager production-mode flag read by install/runtime guards'], + ['NPM_CONFIG_PRODUCTION', 'Package-manager production-mode flag read by install/runtime guards'], ['INIT_CWD', 'Package-manager metadata injected by npm/pnpm; original invocation directory'], ['COGVIDEO_API_KEY', 'F139 MediaHub CogVideoX provider — mcp-server-local credential'], + ['CAT_CAFE_TEST_NODE_VERSION', 'Test-only override for scripts/check-node-runtime.mjs'], + ['CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC', 'Internal recursion guard exported by scripts/lib/node-runtime-guard.sh'], // F240: Per-connector env vars migrated to YAML manifests (connector.yaml / plugin.yaml). // Runtime still reads process.env as fallback in resolveConnectorEnv() chain, but // documentation/display is now driven by the YAML config.fields declarations. @@ -95,10 +99,17 @@ function collectTsFiles(dir) { return results; } +const EXTRA_ENV_REF_FILES = [ + 'scripts/check-node-runtime.mjs', + 'scripts/check-validation-node-runtime.mjs', + 'scripts/lib/node-runtime-guard.sh', +]; + // ── Extract process.env references from source files ── -function extractEnvRefs(dirs) { +function extractEnvRefs(dirs, extraFiles = []) { /** @type {Map} varName → [file:line, ...] */ const refs = new Map(); + const files = []; for (const dir of dirs) { const absDir = join(ROOT, dir); @@ -107,38 +118,48 @@ function extractEnvRefs(dirs) { } catch { continue; } - for (const file of collectTsFiles(absDir)) { - const lines = readFileSync(file, 'utf-8').split('\n'); - let inBlockComment = false; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmed = line.trimStart(); - // Track multi-line block comments - if (inBlockComment) { - if (line.includes('*/')) { - inBlockComment = false; - } - continue; - } - // Single-line block comment: /** ... */ or /* ... */ on one line - if (trimmed.startsWith('/*') && line.includes('*/')) continue; - // Start of multi-line block comment (no closing on same line) - if (trimmed.startsWith('/*')) { - inBlockComment = true; - continue; - } - // Skip pure line comments - if (trimmed.startsWith('//')) continue; - // Strip inline comments before matching (trailing // and inline /* */) - const code = line.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, ''); - // Match process.env.VAR_NAME and process.env['VAR_NAME'] - const dotMatches = code.matchAll(/process\.env\.([A-Za-z_][A-Za-z0-9_]*)/g); - const bracketMatches = code.matchAll(/process\.env\[['"]([A-Za-z_][A-Za-z0-9_]*)['"]\]/g); - for (const m of [...dotMatches, ...bracketMatches]) { - const name = m[1]; - if (!refs.has(name)) refs.set(name, []); - refs.get(name).push(`${file.replace(ROOT + '/', '')}:${i + 1}`); + files.push(...collectTsFiles(absDir)); + } + + for (const file of extraFiles) { + files.push(join(ROOT, file)); + } + + for (const file of files) { + const lines = readFileSync(file, 'utf-8').split('\n'); + let inBlockComment = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trimStart(); + // Track multi-line block comments + if (inBlockComment) { + if (line.includes('*/')) { + inBlockComment = false; } + continue; + } + // Single-line block comment: /** ... */ or /* ... */ on one line + if (trimmed.startsWith('/*') && line.includes('*/')) continue; + // Start of multi-line block comment (no closing on same line) + if (trimmed.startsWith('/*')) { + inBlockComment = true; + continue; + } + // Skip pure line comments + if (trimmed.startsWith('//')) continue; + // Strip inline comments before matching (trailing // and inline /* */) + const code = line.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, ''); + // Match process.env.VAR_NAME and process.env['VAR_NAME'] + const dotMatches = code.matchAll(/process\.env\.([A-Za-z_][A-Za-z0-9_]*)/g); + const bracketMatches = code.matchAll(/process\.env\[['"]([A-Za-z_][A-Za-z0-9_]*)['"]\]/g); + // Match shell ${ENV_VAR:-default} and $ENV_VAR references in guard shell scripts. + const shellMatches = file.endsWith('.sh') + ? code.matchAll(/\$\{([A-Z_][A-Z0-9_]*)[^}]*\}|\$([A-Z_][A-Z0-9_]*)\b/g) + : []; + for (const m of [...dotMatches, ...bracketMatches, ...shellMatches]) { + const name = m[1] ?? m[2]; + if (!refs.has(name)) refs.set(name, []); + refs.get(name).push(`${file.replace(ROOT + '/', '')}:${i + 1}`); } } } @@ -149,7 +170,7 @@ function extractEnvRefs(dirs) { // ── Tests ── describe('env-registry completeness', () => { const registeredNames = loadRegisteredNames(); - const envRefs = extractEnvRefs(['packages/api/src', 'packages/mcp-server/src']); + const envRefs = extractEnvRefs(['packages/api/src', 'packages/mcp-server/src'], EXTRA_ENV_REF_FILES); const repoInboxEnvNames = ['GITHUB_WEBHOOK_SECRET', 'GITHUB_REPO_ALLOWLIST', 'GITHUB_REPO_INBOX_CAT_ID']; const githubSelfFilterEnvNames = ['GITHUB_SELF_LOGIN']; const weixinRuntimeFlagNames = [ @@ -157,6 +178,17 @@ describe('env-registry completeness', () => { 'WEIXIN_ENABLE_UNSAFE_VOICE_MODES', 'WEIXIN_CAPTURE_INBOUND_VOICE_MEDIA', ]; + const nodeRuntimeShellEnvNames = [ + 'CAT_CAFE_NODE_PINNED_MAJOR', + 'CAT_CAFE_NODE_PREFERRED_MAJORS', + 'CAT_CAFE_NODE_BIN', + 'CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC', + ]; + const nodeRuntimeOperatorKnobNames = [ + 'CAT_CAFE_NODE_PINNED_MAJOR', + 'CAT_CAFE_NODE_PREFERRED_MAJORS', + 'CAT_CAFE_NODE_BIN', + ]; it('every allowlist entry has a non-empty reason', () => { for (const [name, reason] of ALLOWLIST) { @@ -185,7 +217,35 @@ describe('env-registry completeness', () => { } }); - it('every process.env.XXX is registered or allowlisted', () => { + it('keeps Node runtime shell guard env vars covered by the env scan', () => { + for (const name of nodeRuntimeShellEnvNames) { + const locations = envRefs.get(name) ?? []; + assert.ok( + locations.some((location) => location.startsWith('scripts/lib/node-runtime-guard.sh:')), + `${name} should be discovered from scripts/lib/node-runtime-guard.sh`, + ); + } + }); + + it('keeps operator-facing Node runtime guard knobs in env-registry', () => { + for (const name of nodeRuntimeOperatorKnobNames) { + assert.ok(registeredNames.has(name), `${name} should be registered for operator discovery`); + assert.ok(!ALLOWLIST.has(name), `${name} is operator-facing config and must not be allowlisted`); + } + }); + + it('keeps the Node runtime guard re-exec sentinel internal', () => { + assert.ok( + ALLOWLIST.has('CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC'), + 'CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC should stay allowlisted as an internal sentinel', + ); + assert.ok( + !registeredNames.has('CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC'), + 'CAT_CAFE_NODE_RUNTIME_GUARD_REEXEC must not be exposed as operator-facing config', + ); + }); + + it('every env reference is registered or allowlisted', () => { const missing = []; for (const [name, locations] of envRefs) { if (!registeredNames.has(name) && !ALLOWLIST.has(name)) { diff --git a/scripts/check-node-runtime.mjs b/scripts/check-node-runtime.mjs index f75b4dbb5b..9dd1f29d35 100644 --- a/scripts/check-node-runtime.mjs +++ b/scripts/check-node-runtime.mjs @@ -16,10 +16,11 @@ if (process.env.CAT_CAFE_SKIP_NODE_RUNTIME_GUARD === '1') { // bypass with CAT_CAFE_SKIP_NODE_RUNTIME_GUARD=1. // This catches the recurring worktree build failure that has hit every cat // for months (Claude Code shell inherits NODE_ENV=production). +const skipProductionInstallGuard = process.env.CAT_CAFE_SKIP_PRODUCTION_INSTALL_GUARD === '1'; const prodEnv = process.env.NODE_ENV === 'production'; const prodFlag = process.env.npm_config_production === 'true' || process.env.NPM_CONFIG_PRODUCTION === 'true'; -if (prodEnv || prodFlag) { +if (!skipProductionInstallGuard && (prodEnv || prodFlag)) { const reason = prodEnv ? 'NODE_ENV=production' : 'npm_config_production=true'; console.error(''); console.error(`[cat-cafe] ❌ ${reason} detected — pnpm will skip devDependencies!`); diff --git a/scripts/check-validation-node-runtime.mjs b/scripts/check-validation-node-runtime.mjs new file mode 100644 index 0000000000..26cd6c127f --- /dev/null +++ b/scripts/check-validation-node-runtime.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +process.env.CAT_CAFE_SKIP_PRODUCTION_INSTALL_GUARD = '1'; + +await import('./check-node-runtime.mjs'); diff --git a/scripts/node-runtime-guard.test.mjs b/scripts/node-runtime-guard.test.mjs index cf6a7d907f..78c8d5ae94 100644 --- a/scripts/node-runtime-guard.test.mjs +++ b/scripts/node-runtime-guard.test.mjs @@ -1,10 +1,21 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from 'node:fs'; import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; +import { join, posix as posixPath, resolve } from 'node:path'; import test from 'node:test'; +const repoRoot = resolve(import.meta.dirname, '..'); + function runBash(snippet, env = {}) { return spawnSync('/bin/bash', ['--noprofile', '--norc', '-c', snippet], { cwd: resolve(import.meta.dirname, '..'), @@ -41,6 +52,53 @@ printf 'fake node ${version}\\n' return path; } +function readJson(relPath) { + return JSON.parse(readFileSync(resolve(repoRoot, relPath), 'utf8')); +} + +function workspacePackageJsonPaths() { + const workspace = readFileSync(resolve(repoRoot, 'pnpm-workspace.yaml'), 'utf8'); + const packageJsonPaths = ['package.json']; + + for (const line of workspace.split(/\r?\n/)) { + const match = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/); + if (!match) continue; + const pattern = match[1]; + if (!pattern.endsWith('/*')) { + throw new Error(`Unsupported workspace package pattern in test: ${pattern}`); + } + + const parent = pattern.slice(0, -2); + for (const entry of readdirSync(resolve(repoRoot, parent), { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const relPath = posixPath.join(parent, entry.name, 'package.json'); + if (existsSync(resolve(repoRoot, relPath))) packageJsonPaths.push(relPath); + } + } + + return packageJsonPaths.sort(); +} + +function isValidationEntrypoint(scriptName) { + const validationEntrypointNames = new Set(['mcp:doctor']); + if (validationEntrypointNames.has(scriptName)) return true; + if (scriptName !== 'prepare' && /^(?:pre|post)/.test(scriptName)) return false; + const validationTokens = new Set(['audit', 'build', 'check', 'gate', 'lint', 'prepare', 'smoke', 'test', 'verify']); + return scriptName.split(':').some((segment) => validationTokens.has(segment) || segment.endsWith('-smoke')); +} + +function validationGuardForPackageJson(relPath) { + if (relPath === 'package.json') return 'node scripts/check-validation-node-runtime.mjs'; + const fromDir = posixPath.dirname(relPath); + const rootPrefix = posixPath.relative(fromDir, '.'); + return `node ${rootPrefix}/scripts/check-validation-node-runtime.mjs`; +} + +function pnpmEngineStrictEnabled() { + const npmrc = readFileSync(resolve(repoRoot, '.npmrc'), 'utf8'); + return npmrc.split(/\r?\n/).some((line) => /^\s*engine-strict\s*=\s*true\s*(?:#.*)?$/.test(line)); +} + test('node runtime guard rejects Node 26 and accepts Node 24', () => { const tmp = mkdtempSync(join(tmpdir(), 'cat-cafe-node-guard-')); try { @@ -238,6 +296,29 @@ test('preinstall guard allows NODE_ENV=production when SKIP guard is set', () => assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`); }); +test('validation scripts can skip production-install guard while keeping Node version guard', () => { + const supported = spawnSync(process.execPath, ['scripts/check-validation-node-runtime.mjs'], { + cwd: resolve(import.meta.dirname, '..'), + encoding: 'utf8', + env: { + CAT_CAFE_TEST_NODE_VERSION: '24.16.0', + NODE_ENV: 'production', + }, + }); + assert.equal(supported.status, 0, `stdout:\n${supported.stdout}\nstderr:\n${supported.stderr}`); + + const unsupported = spawnSync(process.execPath, ['scripts/check-validation-node-runtime.mjs'], { + cwd: resolve(import.meta.dirname, '..'), + encoding: 'utf8', + env: { + CAT_CAFE_TEST_NODE_VERSION: '23.11.0', + NODE_ENV: 'production', + }, + }); + assert.equal(unsupported.status, 1); + assert.match(unsupported.stderr, /Node 23\.11\.0 is not supported/); +}); + test('package engines advertise the Node 24 floor required by recursive workspace tests', () => { const pkg = JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')); @@ -245,6 +326,86 @@ test('package engines advertise the Node 24 floor required by recursive workspac assert.doesNotMatch(pkg.engines.node, /<\s*26/); }); +test('direct validation scripts fail fast on unsupported Node before running package work', () => { + const entries = []; + const missingProtection = []; + + for (const path of workspacePackageJsonPaths()) { + const pkg = readJson(path); + const guard = validationGuardForPackageJson(path); + for (const scriptName of Object.keys(pkg.scripts ?? {}) + .filter(isValidationEntrypoint) + .sort()) { + entries.push(`${path}#${scriptName}`); + const preScript = pkg.scripts[`pre${scriptName}`]; + if (preScript) { + assert.ok(preScript.includes(guard), `${path} pre${scriptName} must run ${guard}`); + assert.doesNotMatch(preScript, /\b[A-Z_]+=1\s+node\b/, `${path} pre${scriptName} must be shell-portable`); + } else { + missingProtection.push(`${path}#${scriptName}`); + } + } + } + + assert.ok( + entries.includes('packages/finance/package.json#lint'), + 'validation entrypoint audit must discover finance lint', + ); + assert.ok( + entries.includes('packages/finance/package.json#prepare'), + 'validation entrypoint audit must discover finance prepare', + ); + assert.ok( + entries.includes('packages/shared/package.json#prepare'), + 'validation entrypoint audit must discover shared prepare', + ); + assert.ok(entries.includes('package.json#mcp:doctor'), 'validation entrypoint audit must discover MCP doctor'); + assert.ok( + entries.includes('packages/api/package.json#test:public'), + 'validation entrypoint audit must discover API test:public', + ); + assert.ok( + entries.includes('packages/web/package.json#test:lint-rules'), + 'validation entrypoint audit must discover web lint-rule tests', + ); + assert.deepEqual( + missingProtection, + [], + `validation entrypoints without node runtime guard:\n${missingProtection.join('\n')}`, + ); +}); + +test('validation entrypoint discovery includes verify, audit, smoke, and suffix test scripts', () => { + assert.equal(isValidationEntrypoint('prepare'), true); + assert.equal(isValidationEntrypoint('mcp:doctor'), true); + assert.equal(isValidationEntrypoint('verify:sigusr1'), true); + assert.equal(isValidationEntrypoint('audit:feature-docs'), true); + assert.equal(isValidationEntrypoint('smoke:f210-agy-profiles'), true); + assert.equal(isValidationEntrypoint('f210:agy-profile-smoke'), true); + assert.equal(isValidationEntrypoint('alpha:test'), true); + assert.equal(isValidationEntrypoint('runtime:test'), true); + + assert.equal(isValidationEntrypoint('preprepare'), false); + assert.equal(isValidationEntrypoint('prebuild'), false); + assert.equal(isValidationEntrypoint('process:doctor'), false); + assert.equal(isValidationEntrypoint('start'), false); + assert.equal(isValidationEntrypoint('start:status'), false); + assert.equal(isValidationEntrypoint('start:direct'), false); + assert.equal(isValidationEntrypoint('dev:direct'), false); +}); + +test('pnpm engine strict remains off so startup scripts can reach the auto-reexec guard', () => { + const pkg = readJson('package.json'); + + assert.equal(pnpmEngineStrictEnabled(), false); + assert.equal(isValidationEntrypoint('start'), false); + assert.equal(isValidationEntrypoint('start:status'), false); + assert.equal(isValidationEntrypoint('start:direct'), false); + assert.equal(isValidationEntrypoint('dev:direct'), false); + assert.match(pkg.scripts.start, /^node \.\/scripts\/start-entry\.mjs start\b/); + assert.match(pkg.scripts['start:status'], /^node \.\/scripts\/start-entry\.mjs status\b/); +}); + test('desktop release workflows install with Node 24 to satisfy the root preinstall guard', () => { const workflowPaths = ['.github/workflows/build-mac-dmg.yml', '.github/workflows/build-windows-desktop.yml'];