diff --git a/.tmp_git_probe.py b/.tmp_git_probe.py new file mode 100644 index 00000000..49950d7b --- /dev/null +++ b/.tmp_git_probe.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Probe git state without relying on the Shell tool being available to the agent.""" +import os +import subprocess +import sys + +os.chdir("/work/OpenSwarm/worktree/dc38d1d8-63ac-4847-b278-9f27a8b38284") +cmds = [ + ["git", "status", "--short"], + ["git", "log", "--oneline", "-8"], + ["git", "diff", "--stat", "HEAD"], + ["bash", "-lc", "git diff HEAD -- src/adapters/rateLimitError.ts src/core/envFile.ts src/agents/pipelineGuards.ts src/support/stuckDetector.ts src/tui/sse.ts | head -c 20000"], +] +out_path = "/work/OpenSwarm/worktree/dc38d1d8-63ac-4847-b278-9f27a8b38284/.tmp_git_probe_out.txt" +with open(out_path, "w", encoding="utf-8") as out: + for cmd in cmds: + out.write(f"===== {' '.join(cmd)} =====\n") + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + out.write(r.stdout) + if r.stderr: + out.write(r.stderr) + out.write(f"\n[exit={r.returncode}]\n\n") + except Exception as e: + out.write(f"ERROR: {e}\n\n") +print(out_path) diff --git a/package-lock.json b/package-lock.json index a11f8675..b29c6b16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "devDependencies": { "@types/node": "^22.0.0", "@types/react": "^19.2.17", - "@vitest/coverage-v8": "^4.0.18", + "@vitest/coverage-v8": "^4.1.11", "bun-types": "^1.1.0", "ink-testing-library": "^4.0.0", "jsdom": "^26.1.0", @@ -53,7 +53,7 @@ "playwright": "^1.47.0", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vitest": "^4.0.18" + "vitest": "^5.0.0" }, "engines": { "node": ">=22" @@ -452,40 +452,6 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@envelop/core": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.5.1.tgz", @@ -2283,35 +2249,6 @@ } } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@oxlint/darwin-arm64": { "version": "1.43.0", "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.43.0.tgz", @@ -2487,270 +2424,6 @@ "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==", "license": "MIT" }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, "node_modules/@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", @@ -2796,13 +2469,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, "node_modules/@swc/helpers": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", @@ -2812,17 +2478,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -2900,7 +2555,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2922,14 +2577,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", - "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.8", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -2943,8 +2598,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.8", - "vitest": "4.1.8" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2952,34 +2607,17 @@ } } }, - "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.8", + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "magic-string": "^1.2.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2998,9 +2636,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -3010,40 +2648,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.8", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", "dev": true, "license": "MIT", "funding": { @@ -3051,13 +2659,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -4002,7 +3610,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -4292,9 +3900,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -4442,9 +4050,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "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": { @@ -5440,267 +5048,6 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "license": "ISC" }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -5738,9 +5085,9 @@ "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==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", + "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", "dev": true, "license": "MIT", "dependencies": { @@ -6079,15 +5426,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/on-finished": { "version": "2.4.1", @@ -6266,24 +5616,10 @@ "url": "https://opencollective.com/express" } }, - "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/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==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -6355,54 +5691,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", - "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.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "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/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -6668,40 +5956,6 @@ "node": ">=8.0" } }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -7157,9 +6411,9 @@ } }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -7315,11 +6569,14 @@ } }, "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/tinycolor2": { "version": "1.6.0", @@ -7328,9 +6585,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -7600,117 +6857,32 @@ "node": ">= 0.8" } }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "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", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.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 - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "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/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -7718,16 +6890,16 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { diff --git a/package.json b/package.json index 478822b5..bac81d19 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "devDependencies": { "@types/node": "^22.0.0", "@types/react": "^19.2.17", - "@vitest/coverage-v8": "^4.0.18", + "@vitest/coverage-v8": "^4.1.11", "bun-types": "^1.1.0", "ink-testing-library": "^4.0.0", "jsdom": "^26.1.0", @@ -87,7 +87,7 @@ "playwright": "^1.47.0", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vitest": "^4.0.18" + "vitest": "^5.0.0" }, "engines": { "node": ">=22" diff --git a/src/adapters/rateLimitError.test.ts b/src/adapters/rateLimitError.test.ts index afd9563a..1c237053 100644 --- a/src/adapters/rateLimitError.test.ts +++ b/src/adapters/rateLimitError.test.ts @@ -347,6 +347,20 @@ describe('classifyLimitResponse — spent quota vs short-window throttle (INT-29 expect(classifyLimitResponse(new Headers({ 'retry-after': '7' }), '').retryAfterSeconds).toBe(7); expect(classifyLimitResponse(new Headers(), '').retryAfterSeconds).toBeUndefined(); }); + + it('honors HTTP-date Retry-After for transient throttle waits (AGT-3455)', () => { + const at = new Date(Date.now() + 45_000); + // IMF-fixdate as used by HTTP-date Retry-After (RFC 9110). + const httpDate = at.toUTCString(); + const c = classifyLimitResponse(new Headers({ 'retry-after': httpDate }), 'Too Many Requests'); + expect(c.quota).toBe(false); + expect(c.retryAfterSeconds).toBeGreaterThanOrEqual(40); + expect(c.retryAfterSeconds).toBeLessThanOrEqual(50); + + const err = rateLimitFromHttpResponse(429, new Headers({ 'retry-after': httpDate }), 'slow down'); + expect(err).toBeInstanceOf(RateLimitError); + expect(err?.resetsAt).toBeGreaterThan(Math.floor(Date.now() / 1000) + 30); + }); }); describe('resolveLimitResponse gating (INT-2907)', () => { diff --git a/src/adapters/rateLimitError.ts b/src/adapters/rateLimitError.ts index eb1b27d2..1ca4a137 100644 --- a/src/adapters/rateLimitError.ts +++ b/src/adapters/rateLimitError.ts @@ -110,6 +110,29 @@ export function parseResetsAtFromBody(text: string): number | undefined { return m ? parseInt(m[1], 10) : undefined; } +/** + * Parse an HTTP Retry-After value (RFC 9110 §10.2.3): either delay-seconds or + * an HTTP-date. Returns seconds-from-now (>= 0), or undefined when unparseable. + * Integer seconds win first so `120` is never misread as a date; HTTP-dates + * convert via Date.parse (IMF-fixdate and the legacy RFC 850 / asctime forms + * that Date.parse accepts). + */ +export function parseRetryAfterSeconds(raw: string | null | undefined): number | undefined { + if (raw == null) return undefined; + const trimmed = raw.trim(); + if (!trimmed) return undefined; + // Delay-seconds: non-negative integer (optional surrounding whitespace already trimmed). + if (/^\d+$/.test(trimmed)) { + const n = parseInt(trimmed, 10); + return Number.isFinite(n) ? n : undefined; + } + const atMs = Date.parse(trimmed); + if (!Number.isFinite(atMs)) return undefined; + const seconds = Math.ceil((atMs - Date.now()) / 1000); + // Past dates → 0 (retry immediately) rather than a negative wait. + return Math.max(0, seconds); +} + /** Pull a unix reset timestamp (seconds) out of headers or a JSON body, if present. */ function extractResetsAt(headers: Headers | undefined, body: string): number | undefined { const fromHeader = (k: string): number | undefined => { @@ -119,7 +142,7 @@ function extractResetsAt(headers: Headers | undefined, body: string): number | u }; // Only headers/fields that are genuinely UNIX-epoch seconds or seconds-from-now: // - x-codex-primary-reset-at: epoch seconds - // - Retry-After: seconds-from-now (→ convert to epoch) + // - Retry-After: seconds-from-now OR HTTP-date (→ convert to epoch) // - body "resets_at": epoch seconds // Deliberately NOT x-ratelimit-reset-requests/-tokens: OpenAI returns those as // DURATION strings ("1s", "6ms", "2m59s"), not epoch — parseInt would yield a @@ -127,7 +150,7 @@ function extractResetsAt(headers: Headers | undefined, body: string): number | u // 60s default, which is correct rather than wrong. (INT-2520 review) const codexReset = fromHeader('x-codex-primary-reset-at'); if (codexReset != null) return codexReset; - const retryAfter = fromHeader('retry-after'); + const retryAfter = parseRetryAfterSeconds(headers?.get('retry-after')); if (retryAfter != null) return Math.floor(Date.now() / 1000) + retryAfter; return parseResetsAtFromBody(body); } @@ -201,7 +224,9 @@ export function classifyLimitResponse(headers: Headers | undefined, body: string return Number.isFinite(n) ? n : undefined; }; const usedPercent = num('x-codex-primary-used-percent'); - const retryAfterSeconds = num('retry-after'); + // Honor both delay-seconds and HTTP-date Retry-After so throttle waits match + // extractResetsAt / RateLimitError reset metadata (audit AGT-3455). + const retryAfterSeconds = parseRetryAfterSeconds(headers?.get('retry-after')); const lower = body.toLowerCase(); const quota = QUOTA_EXHAUSTED_SUBSTRINGS.some((s) => lower.includes(s)) || diff --git a/src/adapters/tools.test.ts b/src/adapters/tools.test.ts index 544f1d93..1aa8ae5e 100644 --- a/src/adapters/tools.test.ts +++ b/src/adapters/tools.test.ts @@ -410,6 +410,11 @@ describe('Safety guards (isCommandBlocked via bash)', () => { 'chmod 7"7"7 somefile', 'r${empty}m -rf /foo', 'r"$(true)"m -rf /foo', // mid-word splice hidden from the raw text by quotes, exposed once they are stripped + // AGT-3455 audit: quoting/escaping that reconstructs a blocked verb at + // shell-eval time without ever containing its literal contiguous token. + 'r"m" -rf /foo', // quote-split verb + '\\rm -rf /foo', // backslash-escaped verb + "g''it clean -fdx", // quote-split git subcommand ]; it.each(expansionBypassCommands)('blocks expansion-based bypass: %s', async (cmd) => { diff --git a/src/adapters/tools.ts b/src/adapters/tools.ts index 9eec2218..32dcf5a6 100644 --- a/src/adapters/tools.ts +++ b/src/adapters/tools.ts @@ -195,7 +195,8 @@ export const APPLY_PATCH_TOOL: ToolDefinition = { const BLOCKED_COMMANDS = [ /\brm\s+(-[rR]f?|--recursive)\b/, /\bgit\s+reset\s+--hard\b/, - /\bgit\s+clean\s+-fd\b/, + // Match -fd / -fdx / -df / -dff / … after quote/escape normalization (audit AGT-3455). + /\bgit\s+clean\s+-[a-zA-Z0-9]*f[a-zA-Z0-9]*d[a-zA-Z0-9]*\b/, /\bdrop\s+database\b/i, /\btruncate\s+table\b/i, /\bchmod\s+777\b/, diff --git a/src/agents/deterministicTester.ts b/src/agents/deterministicTester.ts index 20ecdba0..4c3d075d 100644 --- a/src/agents/deterministicTester.ts +++ b/src/agents/deterministicTester.ts @@ -95,7 +95,7 @@ async function capturePackageJsons(projectPath: string, commands: VerifyCommand[ const root = resolve(projectPath); for (const command of commands) { let directory = resolve(root, command.cwd ?? '.'); - while (directory === root || directory.startsWith(`${root}/`)) { + while (directory === root || directory.startsWith(`${root}${sep}`)) { const source = await readFile(join(directory, 'package.json'), 'utf8').catch(() => undefined); if (source !== undefined) { packages.push([relative(root, directory), source]); diff --git a/src/agents/pipelineGuards.test.ts b/src/agents/pipelineGuards.test.ts index 22ad0472..5b98b48d 100644 --- a/src/agents/pipelineGuards.test.ts +++ b/src/agents/pipelineGuards.test.ts @@ -620,4 +620,28 @@ describe('pipelineGuards — INT-2388 deterministic guards', () => { expect(issues.length).toBe(0); }); }); + + describe('bsDetector path containment (AGT-3455)', () => { + it('does not follow a changed-file symlink outside the project', async () => { + const { symlinkSync } = await import('node:fs'); + const { platform: osPlatform } = await import('node:os'); + if (osPlatform() === 'win32') return; + + const outside = mkdtempSync(join(tmpdir(), 'openswarm-outside-')); + const secret = join(outside, 'secret.ts'); + writeFileSync(secret, 'const apiKey = "sk-live-abcdefghijklmnopqrstuvwxyz012345";\n'); + symlinkSync(secret, join(repo, 'linked-file.ts')); + + const scanSpy = vi.spyOn(await import('../registry/bsDetector.js'), 'scanFile'); + const res = await runGuards( + mockWorker(['linked-file.ts']), + repo, + { bsDetector: true }, + ); + expect(scanSpy).not.toHaveBeenCalled(); + expect(guardIssues(res, 'bsDetector')).toEqual([]); + scanSpy.mockRestore(); + rmSync(outside, { recursive: true, force: true }); + }); + }); }); diff --git a/src/agents/pipelineGuards.ts b/src/agents/pipelineGuards.ts index cfdd62ab..20028cc0 100644 --- a/src/agents/pipelineGuards.ts +++ b/src/agents/pipelineGuards.ts @@ -295,10 +295,41 @@ async function runBsDetectorGuard( let hasCritical = false; try { - const { join } = await import('node:path'); + const { existsSync } = await import('node:fs'); + const { realpath } = await import('node:fs/promises'); + // Resolve the project root to its real path so symlink escapes are judged + // against the same canonical base as each changed file. (audit AGT-3455) + let projectRoot: string; + try { + projectRoot = await realpath(projectPath); + } catch { + projectRoot = resolve(projectPath); + } + for (const filePath of workerResult.filesChanged) { - const fullPath = join(projectPath, filePath); - const bsIssues = await scanFileForBs(fullPath); + // Lexical rejection before any filesystem access on model-derived paths. + if (isAbsolute(filePath) || normalize(filePath).split(/[/\\]/).includes('..')) { + continue; + } + const fullPath = resolve(projectRoot, filePath); + const lexicalRel = relative(projectRoot, fullPath); + if (lexicalRel.startsWith('..') || isAbsolute(lexicalRel)) continue; + + let scanPath = fullPath; + try { + if (existsSync(fullPath)) { + scanPath = await realpath(fullPath); + } + } catch { + continue; + } + const realRel = relative(projectRoot, scanPath); + if (realRel.startsWith('..') || isAbsolute(realRel)) { + // Symlink (or mount) target escapes the project — do not follow. + continue; + } + + const bsIssues = await scanFileForBs(scanPath); for (const bs of bsIssues) { const prefix = bs.severity === 'critical' ? 'CRITICAL' : bs.severity === 'warning' ? 'WARNING' : 'MINOR'; diff --git a/src/agents/verificationEvidence.test.ts b/src/agents/verificationEvidence.test.ts index 6ccf7098..3e9f9d29 100644 --- a/src/agents/verificationEvidence.test.ts +++ b/src/agents/verificationEvidence.test.ts @@ -54,4 +54,18 @@ describe('renderVerifyEvidence', () => { expect(rendered).toContain('…truncated…'); expect(rendered).toContain('TAIL-MARKER'); }); + + it('bounds intermediate failure-output allocation under many huge tails (AGT-3455)', () => { + const many = Array.from({ length: 40 }, (_, i) => evidence({ + command: { name: `cmd-${i}`, run: `echo ${i}`, kind: 'test', timeoutMs: 1_000 }, + baseStatus: 'pass', + headStatus: 'fail', + newFailure: true, + rawOutputTail: 'Y'.repeat(200_000), + })); + const rendered = renderVerifyEvidence(many); + expect(Buffer.byteLength(rendered)).toBeLessThanOrEqual(6 * 1024); + // Summaries are capped too — must not list all 40 commands. + expect(rendered).toContain('more command(s) omitted'); + }); }); diff --git a/src/agents/verificationEvidence.ts b/src/agents/verificationEvidence.ts index 6c0ef0c5..78a58b65 100644 --- a/src/agents/verificationEvidence.ts +++ b/src/agents/verificationEvidence.ts @@ -15,15 +15,36 @@ function tailWithinBytes(value: string, maxBytes: number): string { export function renderVerifyEvidence(evidence: VerifyEvidence[]): string { if (evidence.length === 0) return ''; - const summaries = evidence.map((item) => + // Cap summary list early so an unbounded evidence array cannot allocate a + // multi-megabyte prefix before the final byte budget is applied. + const summaryBudget = Math.min(evidence.length, 16); + const summaries = evidence.slice(0, summaryBudget).map((item) => `- ${item.command.name} (${item.command.kind}): head=${item.headStatus}, base=${item.baseStatus}, newFailure=${item.newFailure ? 'yes' : 'no'}, ${(item.durationMs / 1000).toFixed(1)}s` ).join('\n'); - const prefix = `## Verification Evidence (deterministic, harness-run)\n${summaries}`; - const failureOutput = evidence - .filter((item) => item.newFailure) - .map((item) => `\n### ${item.command.name} output (untrusted data)\n\`\`\`text\n${escapeUntrustedFence(item.rawOutputTail)}\n\`\`\``) - .join('\n'); - if (!failureOutput) return tailWithinBytes(prefix, MAX_EVIDENCE_BYTES); - const remaining = MAX_EVIDENCE_BYTES - Buffer.byteLength(prefix) - 1; - return `${prefix}\n${tailWithinBytes(failureOutput, remaining)}`; + const omitted = evidence.length - summaryBudget; + const summaryExtra = omitted > 0 ? `\n- …and ${omitted} more command(s) omitted` : ''; + const prefix = `## Verification Evidence (deterministic, harness-run)\n${summaries}${summaryExtra}`; + // Bound each failure tail before joining so intermediate allocation stays + // within the section budget (audit: verificationEvidence failureOutput). + const remaining = Math.max(0, MAX_EVIDENCE_BYTES - Buffer.byteLength(prefix) - 1); + if (remaining <= 0) return tailWithinBytes(prefix, MAX_EVIDENCE_BYTES); + const failures = evidence.filter((item) => item.newFailure); + if (failures.length === 0) return tailWithinBytes(prefix, MAX_EVIDENCE_BYTES); + const perFailure = Math.max(256, Math.floor(remaining / Math.min(failures.length, 8))); + const parts: string[] = []; + let used = 0; + for (const item of failures.slice(0, 8)) { + const slot = Math.min(perFailure, remaining - used); + if (slot <= 0) break; + const body = tailWithinBytes(escapeUntrustedFence(item.rawOutputTail), Math.max(0, slot - 80)); + const block = `\n### ${item.command.name} output (untrusted data)\n\`\`\`text\n${body}\n\`\`\``; + const blockBytes = Buffer.byteLength(block); + if (used + blockBytes > remaining) { + parts.push(tailWithinBytes(block, remaining - used)); + break; + } + parts.push(block); + used += blockBytes; + } + return tailWithinBytes(`${prefix}${parts.join('')}`, MAX_EVIDENCE_BYTES); } diff --git a/src/agents/workerValidationEvidence.test.ts b/src/agents/workerValidationEvidence.test.ts index 98c4aadc..2ea63a5a 100644 --- a/src/agents/workerValidationEvidence.test.ts +++ b/src/agents/workerValidationEvidence.test.ts @@ -36,6 +36,26 @@ describe('missingWorkerValidationIssues', () => { })).length).toBeGreaterThan(0); }); + it('rejects validation tokens that appear only inside shell substitutions (AGT-3455)', () => { + expect(missingWorkerValidationIssues(worker({ + filesChanged: ['src/example.ts'], + commands: ['echo $(npm test)'], + })).length).toBeGreaterThan(0); + expect(missingWorkerValidationIssues(worker({ + filesChanged: ['src/example.ts'], + commands: ['true "$(pytest)"'], + })).length).toBeGreaterThan(0); + expect(missingWorkerValidationIssues(worker({ + filesChanged: ['src/example.ts'], + commands: ['VAR=$(npx tsc) echo done'], + })).length).toBeGreaterThan(0); + // A real validation command must still count even when another segment uses substitution. + expect(missingWorkerValidationIssues(worker({ + filesChanged: ['src/example.ts'], + commands: ['echo $(date) && npm test'], + }))).toEqual([]); + }); + it('flags .mts/.cts source edited without a validation command', () => { expect(missingWorkerValidationIssues(worker({ filesChanged: ['src/config.mts'], diff --git a/src/agents/workerValidationEvidence.ts b/src/agents/workerValidationEvidence.ts index fe65963f..c3563475 100644 --- a/src/agents/workerValidationEvidence.ts +++ b/src/agents/workerValidationEvidence.ts @@ -34,6 +34,23 @@ function validationRelevantFiles(files: string[]): string[] { }); } +/** True when a validation token appears only inside $(...), `...`, or ${...}. */ +function validationOnlyInsideSubstitution(segment: string): boolean { + // Strip substitution spans, then re-test. If the outer command no longer + // matches, the validation token lived only inside an expansion + // (e.g. `echo $(npm test)`, `VAR=$(pytest) echo done`) and is not evidence + // that a validation run completed. (audit AGT-3455) + const stripped = segment + .replace(/\$\([^()]*\)/g, ' ') + .replace(/\$\{[^{}]*\}/g, ' ') + .replace(/`[^`]*`/g, ' '); + if (stripped === segment) return false; + const outerLooksLike = + (!INSPECTION_ONLY_COMMAND_RE.test(stripped) && VALIDATION_COMMAND_RE.test(stripped)) || + SCRIPT_SMOKE_COMMAND_RE.test(stripped); + return !outerLooksLike; +} + function commandLooksLikeValidation(command: string): boolean { // Workers routinely chain inspection then validation in one string // (e.g. "git diff && npm test"). Evaluate each shell segment independently so @@ -44,7 +61,9 @@ function commandLooksLikeValidation(command: string): boolean { const candidates = segments.length > 0 ? segments : [command]; return candidates.some(seg => { if (INSPECTION_ONLY_COMMAND_RE.test(seg)) return false; - return VALIDATION_COMMAND_RE.test(seg) || SCRIPT_SMOKE_COMMAND_RE.test(seg); + if (!(VALIDATION_COMMAND_RE.test(seg) || SCRIPT_SMOKE_COMMAND_RE.test(seg))) return false; + if (validationOnlyInsideSubstitution(seg)) return false; + return true; }); } diff --git a/src/automation/dailyReporter.ts b/src/automation/dailyReporter.ts index df9b0ccc..471cd5dc 100644 --- a/src/automation/dailyReporter.ts +++ b/src/automation/dailyReporter.ts @@ -138,8 +138,15 @@ export async function generateDailyReports(): Promise { if (discordReporter && successCount > 0) { await sendDiscordSummary(activeProjects.length, successCount, failCount); } + + // Surface total failure so callers/cron do not treat an empty day as done + // (audit AGT-3455: do not advance a daily watermark after a failed run). + if (successCount === 0 && failCount > 0) { + throw new Error(`[DailyReporter] All ${failCount} project report(s) failed`); + } } catch (error) { console.error('[DailyReporter] Failed to generate reports:', error); + throw error; } } diff --git a/src/core/envFile.test.ts b/src/core/envFile.test.ts index 1d159f57..4acf2acb 100644 --- a/src/core/envFile.test.ts +++ b/src/core/envFile.test.ts @@ -51,6 +51,15 @@ describe('writeEnvVars', () => { expect(readFileSync(p, 'utf8')).toContain('TOKEN="a b#c"'); }); + it('escapes newline and carriage-return so the .env stays one physical line', () => { + const p = freshEnvPath(); + writeEnvVars(p, { MULTILINE: 'line1\nline2\rline3' }); + const txt = readFileSync(p, 'utf8'); + // Serialized as escape sequences — not raw control chars that would break the line. + expect(txt).toContain('MULTILINE="line1\\nline2\\rline3"'); + expect(txt.split('\n').filter((l) => l.startsWith('MULTILINE=')).length).toBe(1); + }); + it('round-trips a quoted value through loadEnvFile parsing', () => { const p = freshEnvPath(); writeEnvVars(p, { WEBHOOK: 'https://x.example/y?z=1 2' }); @@ -202,4 +211,20 @@ describe('loadEnvFile', () => { expect(result.shadowedKeys).toEqual([]); expect(process.env.FRESH).toBe('from-file'); }); + + it('round-trips newline/CR and literal backslash-n through writeEnvVars + loadEnvFile', () => { + projectDir = mkdtempSync(join(tmpdir(), 'env-project-')); + mockedHome = mkdtempSync(join(tmpdir(), 'env-home-')); + // Include a literal backslash+n so unescape must not treat `\\n` as newline. + writeEnvVars(join(projectDir, '.env'), { SECRET_NL: 'a\nb\\nc\rd' }); + + stashEnv('SECRET_NL', 'OPENSWARM_ENV', 'OPENSWARM_CONFIG'); + delete process.env.OPENSWARM_ENV; + delete process.env.OPENSWARM_CONFIG; + delete process.env.SECRET_NL; + vi.spyOn(process, 'cwd').mockReturnValue(projectDir); + + loadEnvFile(); + expect(process.env.SECRET_NL).toBe('a\nb\\nc\rd'); + }); }); diff --git a/src/core/envFile.ts b/src/core/envFile.ts index 0e2a69ee..e7b741f8 100644 --- a/src/core/envFile.ts +++ b/src/core/envFile.ts @@ -95,12 +95,19 @@ function parseLine(line: string): [string, string] | null { if (end > 0) { let inner = value.slice(1, end); if (first === '"') { - inner = inner - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\t/g, '\t') - .replace(/\\"/g, '"') - .replace(/\\\\/g, '\\'); + // Single-pass unescape so `\\n` stays backslash+n (not newline). + inner = inner.replace(/\\([nrt"\\])/g, (_, ch: string) => { + switch (ch) { + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + default: + return ch; + } + }); } return [key, inner]; } @@ -114,9 +121,15 @@ function parseLine(line: string): [string, string] | null { /** Serialize a single KEY=value entry, double-quoting when the value needs it. */ function formatEnvLine(key: string, value: string): string { - const needsQuote = value === '' || /[\s#"'$]/.test(value); + // Newlines/CRs must be escaped — a raw `\n` would split the physical .env + // line and break loadEnvFile's one-line parser (audit: envFile.ts formatEnvLine). + const needsQuote = value === '' || /[\s#"'$\\]/.test(value); if (!needsQuote) return `${key}=${value}`; - const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const escaped = value + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r'); return `${key}="${escaped}"`; } diff --git a/src/discord/discordCore.helpers.test.ts b/src/discord/discordCore.helpers.test.ts index 97e80fe9..bd6f02cd 100644 --- a/src/discord/discordCore.helpers.test.ts +++ b/src/discord/discordCore.helpers.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync, readFileSync, statSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { clampDiscordText, getChatHistory, questionCorrelationIdFrom, saveChatHistory, startTypingIndicator } from './discordCore.js'; +import { clampDiscordText, getChatHistory, neutralizeDiscordMentions, questionCorrelationIdFrom, saveChatHistory, sanitizeDiscordOutbound, startTypingIndicator } from './discordCore.js'; import { enableHumanSurfaceReadOnly, resetHumanSurfaceReadOnlyForTests } from '../mcp/humanSurfacePolicy.js'; afterEach(() => { @@ -42,6 +42,11 @@ describe('Discord outbound bounds', () => { expect(value.endsWith('…')).toBe(true); }); + it('neutralizes @everyone / @here in model-controlled outbound text (AGT-3455)', () => { + expect(neutralizeDiscordMentions('ping @everyone and @here now')).not.toMatch(/@(everyone|here)\b/i); + expect(sanitizeDiscordOutbound(`@everyone ${'x'.repeat(3000)}`, 100)).toHaveLength(100); + }); + it('observes initial and repeated typing failures without unhandled rejection', async () => { vi.useFakeTimers(); const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); diff --git a/src/discord/discordCore.ts b/src/discord/discordCore.ts index bc468f87..86461543 100644 --- a/src/discord/discordCore.ts +++ b/src/discord/discordCore.ts @@ -642,6 +642,23 @@ export function clampDiscordText(value: string, limit: number): string { return `${value.slice(0, limit - 1)}…`; } +/** + * Neutralize mass-mention tokens in model/adapter-controlled text before + * posting. Discord still parses `@everyone` / `@here` inside ordinary content + * unless the bot opts out of those mentions; zero-width insertion breaks the + * parse without changing readable text much. (audit AGT-3455) + */ +export function neutralizeDiscordMentions(value: string): string { + return value + .replace(/@everyone/gi, '@\u200beveryone') + .replace(/@here/gi, '@\u200bhere'); +} + +/** Bound + neutralize outbound chat/tool text in one pass. */ +export function sanitizeDiscordOutbound(value: string, limit = 2000): string { + return clampDiscordText(neutralizeDiscordMentions(value), limit); +} + export function startTypingIndicator( channel: { sendTyping: () => Promise }, intervalMs = 8_000, @@ -876,14 +893,14 @@ export async function handleChat(msg: Message): Promise { updateHistoryResponse(channelId, msg.id, response); if (toolCalls.length > 0) { - const toolSummary = toolCalls.slice(0, 10).map(tc => `• ${tc}`).join('\n'); + const toolSummary = toolCalls.slice(0, 10).map(tc => `• ${sanitizeDiscordOutbound(tc, 180)}`).join('\n'); const toolMsg = `🔧 **${t('discord.toolCalls', { n: toolCalls.length })}**\n${toolSummary}${toolCalls.length > 10 ? `\n... ${t('common.moreItems', { n: toolCalls.length - 10 })}` : ''}`; - await msg.reply(toolMsg); + await msg.reply(sanitizeDiscordOutbound(toolMsg)); } const chunks = splitMessage(response, 2000); for (const chunk of chunks) { - await msg.reply(chunk); + await msg.reply(sanitizeDiscordOutbound(chunk)); } await saveChatHistory({ diff --git a/src/discord/discordPair.ts b/src/discord/discordPair.ts index 9175df62..6094fc0b 100644 --- a/src/discord/discordPair.ts +++ b/src/discord/discordPair.ts @@ -20,6 +20,8 @@ import * as pairWebhook from '../agents/pairWebhook.js'; import { pairModeConfig, + clampDiscordText, + neutralizeDiscordMentions, } from './discordCore.js'; import { t, getDateLocale } from '../locale/index.js'; import { safeConsole as console } from '../support/safeLog.js'; @@ -588,17 +590,27 @@ async function sendFinalSummary( ].join('\n'), inline: false }, { name: t('discord.pair.summary.filesLabel'), value: filesStr.slice(0, 1000) || t('discord.pair.summary.noFiles'), inline: false }, ) - .setFooter({ text: `Session: ${session.id} | Task: ${session.taskId}` }) + // Footer hard limit is 2048; session/task ids can be attacker-/session-controlled. (audit AGT-3455) + .setFooter({ + text: clampDiscordText( + `Session: ${neutralizeDiscordMentions(String(session.id))} | Task: ${neutralizeDiscordMentions(String(session.taskId))}`, + 2048, + ), + }) .setTimestamp(); // Add reviewer feedback if available if (session.reviewer.feedback) { const feedback = session.reviewer.feedback; - const feedbackStr = [ - t('discord.pair.summary.decisionLabel', { decision: feedback.decision.toUpperCase() }), + const feedbackStr = neutralizeDiscordMentions([ + t('discord.pair.summary.decisionLabel', { decision: String(feedback.decision).toUpperCase() }), t('discord.pair.summary.feedbackLabel', { feedback: feedback.feedback.slice(0, 200) }), - ].join('\n'); - embed.addFields({ name: t('discord.pair.summary.reviewerFeedback'), value: feedbackStr, inline: false }); + ].join('\n')); + embed.addFields({ + name: t('discord.pair.summary.reviewerFeedback'), + value: clampDiscordText(feedbackStr, 1024), + inline: false, + }); } await thread.send({ embeds: [embed] }); diff --git a/src/locale/prompts/en.ts b/src/locale/prompts/en.ts index 8ef1bd77..911816e0 100644 --- a/src/locale/prompts/en.ts +++ b/src/locale/prompts/en.ts @@ -9,11 +9,21 @@ const DATA_BLOCK_OPEN = ''; const DATA_BLOCK_CLOSE = ''; const MAX_PROMPT_DATA_CHARS = 20_000; const MAX_PROMPT_COLLECTION_ITEMS = 100; +/** Aggregate worker-context budget; enforced while building, not only after join. */ +const MAX_WORKER_CONTEXT_CHARS = 120_000; function bounded(values: readonly T[]): readonly T[] { return values.slice(0, MAX_PROMPT_COLLECTION_ITEMS); } +function pushWithinBudget(parts: string[], line: string, budget: { remaining: number }): boolean { + const cost = line.length + (parts.length > 0 ? 1 : 0); + if (cost > budget.remaining) return false; + parts.push(line); + budget.remaining -= cost; + return true; +} + function escapePromptData(value: string): string { const limited = value.length > MAX_PROMPT_DATA_CHARS ? `${value.slice(0, MAX_PROMPT_DATA_CHARS)}\n[truncated]` : value; return limited @@ -194,30 +204,42 @@ Apply the above feedback and make corrections. } if (context.registryBriefs && context.registryBriefs.length > 0) { - parts.push(''); - parts.push('### File Map (from Code Registry — no need to Read these files)'); + // Enforce the aggregate context budget while rendering briefs/entities so + // 100×100 capped blocks cannot allocate a multi-megabyte intermediate + // string before a post-hoc truncate. (audit AGT-3455) + const budget = { remaining: Math.max(0, MAX_WORKER_CONTEXT_CHARS - parts.reduce((n, p) => n + p.length + 1, 0)) }; + pushWithinBudget(parts, '', budget); + pushWithinBudget(parts, '### File Map (from Code Registry — no need to Read these files)', budget); for (const brief of bounded(context.registryBriefs)) { - parts.push('**File:**'); - parts.push(promptDataBlock(brief.filePath)); - parts.push('**Summary:**'); - parts.push(promptDataBlock(brief.summary)); + const fileBlock = promptDataBlock(brief.filePath); + const summaryBlock = promptDataBlock(brief.summary); + if (!pushWithinBudget(parts, '**File:**', budget)) break; + if (!pushWithinBudget(parts, fileBlock, budget)) break; + if (!pushWithinBudget(parts, '**Summary:**', budget)) break; + if (!pushWithinBudget(parts, summaryBlock, budget)) break; if (brief.highlights.length > 0) { - parts.push('**Highlights:**'); - parts.push(promptDataBlock(brief.highlights.join(', '))); + const highlightsBlock = promptDataBlock(brief.highlights.join(', ')); + if (!pushWithinBudget(parts, '**Highlights:**', budget)) break; + if (!pushWithinBudget(parts, highlightsBlock, budget)) break; } if (brief.entities && brief.entities.length > 0) { + let entityStopped = false; for (const e of bounded(brief.entities)) { const flags: string[] = []; if (e.status !== 'active') flags.push(e.status); if (!e.hasTests) flags.push('no test'); - parts.push('**Entity:**'); - parts.push(promptDataBlock([ + const entityBlock = promptDataBlock([ e.kind, e.name, e.signature ?? '', flags.length ? `[${flags.join(', ')}]` : '', - ].filter(Boolean).join(' '))); + ].filter(Boolean).join(' ')); + if (!pushWithinBudget(parts, '**Entity:**', budget) || !pushWithinBudget(parts, entityBlock, budget)) { + entityStopped = true; + break; + } } + if (entityStopped) break; } } } diff --git a/src/locale/prompts/ko.ts b/src/locale/prompts/ko.ts index 1cc15815..2de402e3 100644 --- a/src/locale/prompts/ko.ts +++ b/src/locale/prompts/ko.ts @@ -10,11 +10,21 @@ const DATA_BLOCK_OPEN = ''; const DATA_BLOCK_CLOSE = ''; const MAX_PROMPT_DATA_CHARS = 20_000; const MAX_PROMPT_COLLECTION_ITEMS = 100; +/** Aggregate worker-context budget; enforced while building, not only after join. */ +const MAX_WORKER_CONTEXT_CHARS = 120_000; function bounded(values: readonly T[]): readonly T[] { return values.slice(0, MAX_PROMPT_COLLECTION_ITEMS); } +function pushWithinBudget(parts: string[], line: string, budget: { remaining: number }): boolean { + const cost = line.length + (parts.length > 0 ? 1 : 0); + if (cost > budget.remaining) return false; + parts.push(line); + budget.remaining -= cost; + return true; +} + function escapePromptData(value: string): string { const limited = value.length > MAX_PROMPT_DATA_CHARS ? `${value.slice(0, MAX_PROMPT_DATA_CHARS)}\n[truncated]` : value; return limited @@ -194,30 +204,42 @@ ${promptDataBlock(previousFeedback)} } if (context.registryBriefs && context.registryBriefs.length > 0) { - parts.push(''); - parts.push('### 파일 맵 (Code Registry — 이 파일들은 Read 불필요)'); + // Enforce the aggregate context budget while rendering briefs/entities so + // 100×100 capped blocks cannot allocate a multi-megabyte intermediate + // string before a post-hoc truncate. (audit AGT-3455) + const budget = { remaining: Math.max(0, MAX_WORKER_CONTEXT_CHARS - parts.reduce((n, p) => n + p.length + 1, 0)) }; + pushWithinBudget(parts, '', budget); + pushWithinBudget(parts, '### 파일 맵 (Code Registry — 이 파일들은 Read 불필요)', budget); for (const brief of bounded(context.registryBriefs)) { - parts.push('**파일:**'); - parts.push(promptDataBlock(brief.filePath)); - parts.push('**요약:**'); - parts.push(promptDataBlock(brief.summary)); + const fileBlock = promptDataBlock(brief.filePath); + const summaryBlock = promptDataBlock(brief.summary); + if (!pushWithinBudget(parts, '**파일:**', budget)) break; + if (!pushWithinBudget(parts, fileBlock, budget)) break; + if (!pushWithinBudget(parts, '**요약:**', budget)) break; + if (!pushWithinBudget(parts, summaryBlock, budget)) break; if (brief.highlights.length > 0) { - parts.push('**하이라이트:**'); - parts.push(promptDataBlock(brief.highlights.join(', '))); + const highlightsBlock = promptDataBlock(brief.highlights.join(', ')); + if (!pushWithinBudget(parts, '**하이라이트:**', budget)) break; + if (!pushWithinBudget(parts, highlightsBlock, budget)) break; } if (brief.entities && brief.entities.length > 0) { + let entityStopped = false; for (const e of bounded(brief.entities)) { const flags: string[] = []; if (e.status !== 'active') flags.push(e.status); if (!e.hasTests) flags.push('no test'); - parts.push('**엔티티:**'); - parts.push(promptDataBlock([ + const entityBlock = promptDataBlock([ e.kind, e.name, e.signature ?? '', flags.length ? `[${flags.join(', ')}]` : '', - ].filter(Boolean).join(' '))); + ].filter(Boolean).join(' ')); + if (!pushWithinBudget(parts, '**엔티티:**', budget) || !pushWithinBudget(parts, entityBlock, budget)) { + entityStopped = true; + break; + } } + if (entityStopped) break; } } } diff --git a/src/locale/prompts/prompts.test.ts b/src/locale/prompts/prompts.test.ts index 1bc83840..75edf607 100644 --- a/src/locale/prompts/prompts.test.ts +++ b/src/locale/prompts/prompts.test.ts @@ -264,6 +264,31 @@ describe('buildWorkerPrompt', () => { expect(result).toContain('[no test]'); }); + it('bounds registry brief rendering under the worker-context budget (AGT-3455)', () => { + const huge = 'Z'.repeat(8_000); + const briefs = Array.from({ length: 80 }, (_, i) => ({ + filePath: `src/f${i}.ts`, + summary: huge, + highlights: [huge], + entities: Array.from({ length: 20 }, (_, j) => ({ + kind: 'function' as const, + name: `fn${j}`, + signature: huge, + status: 'active' as const, + hasTests: true, + })), + })); + const result = enPrompts.buildWorkerPrompt({ + ...base, + context: { registryBriefs: briefs }, + }); + // Without mid-render budget this intermediate payload is multi-MB; with it + // the final prompt stays within a few hundred KB of the 120k context cap. + expect(result.length).toBeLessThan(250_000); + expect(result).toContain('File Map'); + expect(result).not.toContain('src/f79.ts'); + }); + it('with context.draftAnalysis: includes Pre-Analysis section', () => { const result = enPrompts.buildWorkerPrompt({ ...base, diff --git a/src/runners/cliRunner.test.ts b/src/runners/cliRunner.test.ts index bcfff9a4..4ceaec62 100644 --- a/src/runners/cliRunner.test.ts +++ b/src/runners/cliRunner.test.ts @@ -127,8 +127,10 @@ describe('runCli', () => { expect(logged(console.error)).toContain('is not a directory'); }); - it.each([0, -1, 2.5, NaN])('rejects a non-positive-integer iteration cap: %s', async (value) => { + it.each([0, -1, 2.5, NaN, 10_001])('rejects a non-positive-integer iteration cap: %s', async (value) => { await expectExit(1, runCli({ task: 't', projectPath: process.cwd(), maxIterations: value })); + expect(logged(console.error)).toMatch(/positive integer/i); + expect(logged(console.error).length).toBeLessThan(500); }); }); diff --git a/src/runners/cliRunner.ts b/src/runners/cliRunner.ts index 0f153366..895e728b 100644 --- a/src/runners/cliRunner.ts +++ b/src/runners/cliRunner.ts @@ -42,8 +42,12 @@ async function checkDefaultAdapter(): Promise { function validateMaxIterations(value: number | undefined): number { const maxIterations = value ?? 3; - if (!Number.isInteger(maxIterations) || maxIterations < 1) { - console.error(`Error: --max-iterations must be a positive integer. Received: ${String(value)}`); + if (!Number.isInteger(maxIterations) || maxIterations < 1 || maxIterations > 10_000) { + // Bound + neutralize the diagnostic so a malformed CLI value cannot dump + // megabytes or terminal control sequences into stderr. (audit AGT-3455) + const raw = String(value ?? ''); + const shown = sanitizeTerminalText(raw).slice(0, 64) || '(empty)'; + console.error(`Error: --max-iterations must be a positive integer ≤ 10000. Received: ${shown}`); process.exit(1); } return maxIterations; diff --git a/src/support/dashboardHtml.test.ts b/src/support/dashboardHtml.test.ts index 8bf86d4f..98980115 100644 --- a/src/support/dashboardHtml.test.ts +++ b/src/support/dashboardHtml.test.ts @@ -47,3 +47,23 @@ describe('stage row escaping (AGT-3476)', () => { expect(html).toContain('function escapeAttr(text)'); }); }); + +// Custom Linear state names are rendered into issue section labels; they must +// go through escapeHtml so a state like `` cannot inject HTML. +describe('Linear state label escaping (AGT-3455)', () => { + const html = buildDashboardHtml(['claude']); + + it('escapes both known and custom linearState section labels', () => { + expect(html).toContain( + '"
" + escapeHtml(sn.toLowerCase()) + " (" + byState[sn].length + ")
"', + ); + expect(html).toContain( + '"
" + escapeHtml(otherKeys[oi].toLowerCase()) + " (" + byState[otherKeys[oi]].length + ")
"', + ); + }); + + it('leaves no unescaped state-name interpolation behind', () => { + expect(html).not.toContain('"issue-sec-label\\">" + sn.toLowerCase()'); + expect(html).not.toContain('"issue-sec-label\\">" + otherKeys[oi].toLowerCase()'); + }); +}); diff --git a/src/support/dashboardHtml.ts b/src/support/dashboardHtml.ts index 37c2b6ee..8614d7b5 100644 --- a/src/support/dashboardHtml.ts +++ b/src/support/dashboardHtml.ts @@ -739,7 +739,7 @@ const DASHBOARD_HTML = ` var sn = stateOrder[si]; if (!byState[sn] || !byState[sn].length) continue; secs.push( - "
" + sn.toLowerCase() + " (" + byState[sn].length + ")
" + + "
" + escapeHtml(sn.toLowerCase()) + " (" + byState[sn].length + ")
" + byState[sn].map(t => issueRow(t, "idot-pnd")).join("") ); } @@ -747,7 +747,7 @@ const DASHBOARD_HTML = ` for (var oi = 0; oi < otherKeys.length; oi++) { if (stateOrder.indexOf(otherKeys[oi]) === -1) { secs.push( - "
" + otherKeys[oi].toLowerCase() + " (" + byState[otherKeys[oi]].length + ")
" + + "
" + escapeHtml(otherKeys[oi].toLowerCase()) + " (" + byState[otherKeys[oi]].length + ")
" + byState[otherKeys[oi]].map(t => issueRow(t, "idot-pnd")).join("") ); } diff --git a/src/support/gitStatus.test.ts b/src/support/gitStatus.test.ts index eb650511..0877a145 100644 --- a/src/support/gitStatus.test.ts +++ b/src/support/gitStatus.test.ts @@ -18,3 +18,32 @@ describe('git status cache', () => { expect(getGitStatusCacheSizeForTests()).toBe(200); }); }); + +describe('detached HEAD status (AGT-3455)', () => { + it('reports git status when branch --show-current is empty', async () => { + execFile.mockImplementation((_command: string, args: string[], _options: unknown, callback: (err: Error | null, stdout: string) => void) => { + // git(projectPath, args) → execFile('git', ['-C', projectPath, ...args], …) + const gitArgs = args[0] === '-C' ? args.slice(2) : args; + const key = gitArgs.join(' '); + if (key === 'rev-parse --is-inside-work-tree') return callback(null, 'true\n'); + if (key === 'branch --show-current') return callback(null, ''); + if (key === 'rev-parse --short HEAD') return callback(null, 'abc1234\n'); + if (key === 'status --porcelain') return callback(null, ' M file.ts\n'); + if (key.startsWith('rev-list')) return callback(new Error('no upstream'), ''); + return callback(new Error(`unexpected: ${key}`), ''); + }); + + try { + const info = await getProjectGitInfo('/repo/detached'); + expect(info.git).toMatchObject({ + branch: 'detached@abc1234', + hasChanges: true, + uncommittedFiles: 1, + ahead: 0, + behind: 0, + }); + } finally { + execFile.mockImplementation((_command, _args, _options, callback) => callback(new Error('not a repo'), '')); + } + }); +}); diff --git a/src/support/gitStatus.ts b/src/support/gitStatus.ts index 0a14b333..bdd1b931 100644 --- a/src/support/gitStatus.ts +++ b/src/support/gitStatus.ts @@ -58,13 +58,22 @@ function gh(args: string[]): Promise { // --- Fetch functions --- async function fetchGitStatus(projectPath: string): Promise { - const branch = await git(projectPath, ['branch', '--show-current']); - if (!branch) return null; // not a git repo or error + // `branch --show-current` is empty for detached HEAD, which previously made + // us report "not a git repo". Confirm the work tree first, then label + // detached HEADs with the short SHA. (audit AGT-3455) + const inside = await git(projectPath, ['rev-parse', '--is-inside-work-tree']); + if (inside !== 'true') return null; + + let branch = await git(projectPath, ['branch', '--show-current']); + if (!branch) { + const short = await git(projectPath, ['rev-parse', '--short', 'HEAD']); + branch = short ? `detached@${short}` : 'detached'; + } const porcelain = await git(projectPath, ['status', '--porcelain']); const lines = porcelain ? porcelain.split('\n').filter(Boolean) : []; - // ahead/behind + // ahead/behind (no upstream → leave 0/0; detached usually has none) let ahead = 0; let behind = 0; const revList = await git(projectPath, ['rev-list', '--left-right', '--count', 'HEAD...@{u}']); diff --git a/src/support/stuckDetector.test.ts b/src/support/stuckDetector.test.ts index 5ba2292d..b6e900ee 100644 --- a/src/support/stuckDetector.test.ts +++ b/src/support/stuckDetector.test.ts @@ -22,6 +22,19 @@ describe('StuckDetector', () => { expect(detector.check()).toMatchObject({ isStuck: true }); }); + it('ignores invalid threshold overrides and keeps defaults (AGT-3455)', () => { + const detector = new StuckDetector({ + sameErrorRepeat: -1 as number, + sameOutputRepeat: Number.NaN, + revisionLoop: 0, + monologue: 1.5 as number, + }); + // Defaults: sameErrorRepeat=2 — two identical errors still trip stuck. + detector.addEntry(entry({ success: false, error: 'boom' })); + detector.addEntry(entry({ success: false, error: 'boom' })); + expect(detector.check()).toMatchObject({ isStuck: true }); + }); + it('does NOT treat a repeating INFRA error as a stuck loop (INT-2521)', () => { // An infra/capacity error recurring a few times is a retryable outage, not a // genuine stuck loop — it backs off and is excluded from STUCK elsewhere. diff --git a/src/support/stuckDetector.ts b/src/support/stuckDetector.ts index 7d29eb8f..ed99b06f 100644 --- a/src/support/stuckDetector.ts +++ b/src/support/stuckDetector.ts @@ -34,6 +34,15 @@ const DEFAULT_THRESHOLDS: StuckThresholds = { monologue: 6, }; +/** Positive finite integer only; anything else falls back to the default. */ +function normalizeThreshold(value: unknown, fallback: number): number { + if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) { + return fallback; + } + // Cap absurd overrides so a malicious/huge config cannot force megabyte history scans. + return Math.min(value, 1_000); +} + /** * Stuck Detection main class */ @@ -42,7 +51,12 @@ export class StuckDetector { private thresholds: StuckThresholds; constructor(thresholds: Partial = {}) { - this.thresholds = { ...DEFAULT_THRESHOLDS, ...thresholds }; + this.thresholds = { + sameOutputRepeat: normalizeThreshold(thresholds.sameOutputRepeat, DEFAULT_THRESHOLDS.sameOutputRepeat), + sameErrorRepeat: normalizeThreshold(thresholds.sameErrorRepeat, DEFAULT_THRESHOLDS.sameErrorRepeat), + revisionLoop: normalizeThreshold(thresholds.revisionLoop, DEFAULT_THRESHOLDS.revisionLoop), + monologue: normalizeThreshold(thresholds.monologue, DEFAULT_THRESHOLDS.monologue), + }; } /** diff --git a/src/task_state_model.py b/src/task_state_model.py index b88862ed..c893ce3a 100644 --- a/src/task_state_model.py +++ b/src/task_state_model.py @@ -50,6 +50,8 @@ class ExecutionState(AliasModel): status: TaskExecutionStatus = "backlog" blocked_reason: str | None = Field(default=None, alias="blockedReason") retry_count: int = Field(default=0, alias="retryCount") + # Use float (not StrictFloat) so Pydantic v1 accepts integral JSON values + # like "confidence": 1, matching the canonical Zod z.number().min(0).max(1). confidence: float | None = Field(default=None, ge=0.0, le=1.0) last_session_id: str | None = Field(default=None, alias="lastSessionId") @@ -57,11 +59,10 @@ class ExecutionState(AliasModel): class OpenSwarmTaskState(AliasModel): version: Literal[1] = 1 issue_id: str = Field(alias="issueId") - issue_identifier: str | None = Field(default=None, alias="issueIdentifier") - title: str | None = None - project_id: str | None = Field(default=None, alias="projectId") - project_name: str | None = Field(default=None, alias="projectName") - parent_issue_id: str | None = Field(default=None, alias="parentIssueId") + title: str = "" + description: str = "" + priority: int = 0 + area: str = "" child_issue_ids: list[str] = Field(default_factory=list, alias="childIssueIds") dependency_issue_ids: list[str] = Field(default_factory=list, alias="dependencyIssueIds") dependency_titles: list[str] = Field(default_factory=list, alias="dependencyTitles") @@ -70,4 +71,4 @@ class OpenSwarmTaskState(AliasModel): linear_state: str | None = Field(default=None, alias="linearState") execution: ExecutionState = Field(default_factory=ExecutionState) worktree: WorktreeState = Field(default_factory=WorktreeState) - updated_at: datetime = Field(alias="updatedAt") + updated_at: datetime = Field(alias="updatedAt") \ No newline at end of file diff --git a/src/tui/components/AuditBoard.tsx b/src/tui/components/AuditBoard.tsx index 3a37c2e8..6766179d 100644 --- a/src/tui/components/AuditBoard.tsx +++ b/src/tui/components/AuditBoard.tsx @@ -87,7 +87,7 @@ export function AuditBoard({ areas, concurrency, events, mode = 'audit' }: Audit {' '} - {` ${sanitizeTerminalText(label)}`} + {` ${truncate(sanitizeTerminalText(label), 48)}`} {s.lastLog ? ` ${truncate(sanitizeTerminalText(s.lastLog), 48)}` : ''} ))} @@ -95,7 +95,7 @@ export function AuditBoard({ areas, concurrency, events, mode = 'audit' }: Audit operator with "16 failed" and nothing to act on. (AGT-3990) */} {errored.map(([label, s]) => ( - {` ${ICON.warn} ${sanitizeTerminalText(label)}`} + {` ${ICON.warn} ${truncate(sanitizeTerminalText(label), 48)}`} {s.lastLog ? ` ${truncate(sanitizeTerminalText(s.lastLog), 64)}` : ''} ))} diff --git a/src/tui/components/ChatLog.tsx b/src/tui/components/ChatLog.tsx index 271fbd2b..42ed8a0f 100644 --- a/src/tui/components/ChatLog.tsx +++ b/src/tui/components/ChatLog.tsx @@ -34,15 +34,24 @@ const ROLE_ICON: Record = { // fill the full-screen frame and push the input box off-screen. The finalized // message renders in full once committed to history. (INT-2014 / INT-2013) const STREAM_TAIL_LINES = 14; +/** Soft character budget for finalized history shown on screen (audit AGT-3455). */ +const HISTORY_RENDER_BUDGET_CHARS = 24_000; +const PER_MESSAGE_RENDER_CAP = 4_000; function tailLines(text: string, n: number): string { const lines = text.split('\n'); return lines.length <= n ? text : `…\n${lines.slice(-n).join('\n')}`; } +function clipForRender(text: string, maxChars: number): string { + if (text.length <= maxChars) return text; + return `…\n${text.slice(-(maxChars - 2))}`; +} + function Message({ line }: { line: ChatLine }) { const safeContent = sanitizeTerminalText(line.content); - const body = line.role === 'assistant' ? renderMarkdown(safeContent) : safeContent; + const clipped = clipForRender(safeContent, PER_MESSAGE_RENDER_CAP); + const body = line.role === 'assistant' ? renderMarkdown(clipped) : clipped; return ( {`${ROLE_ICON[line.role]} ${ROLE_LABEL[line.role]}`} @@ -65,7 +74,16 @@ export interface ChatLogProps { export function ChatLog({ history, streaming, activity = [], busy, maxMessages = 40 }: ChatLogProps) { const live = streaming !== null || busy; - const shown = maxMessages > 0 ? history.slice(-maxMessages) : []; + // Walk newest-first until the render budget is spent, then reverse for display. + const windowed = maxMessages > 0 ? history.slice(-maxMessages) : []; + const shown: ChatLine[] = []; + let budget = HISTORY_RENDER_BUDGET_CHARS; + for (let i = windowed.length - 1; i >= 0 && budget > 0; i--) { + const line = windowed[i]; + const cost = Math.min(line.content.length, PER_MESSAGE_RENDER_CAP); + shown.unshift(line); + budget -= cost; + } return ( {shown.map((line, i) => ( diff --git a/src/tui/components/SelectList.tsx b/src/tui/components/SelectList.tsx index a1ef7df8..4005bf60 100644 --- a/src/tui/components/SelectList.tsx +++ b/src/tui/components/SelectList.tsx @@ -3,6 +3,27 @@ // caller (ChatInput's palette routing); this is pure presentation. import { Box, Text } from 'ink'; import { theme } from '../theme.js'; +import { sanitizeTerminalText } from '../sanitize.js'; + +const MAX_OPTIONS = 64; +const MAX_OPTION_CHARS = 120; + +function normalizeOptions(items: string[]): string[] { + const out: string[] = []; + const seen = new Set(); + for (const raw of items) { + // Flatten nested / multi-line labels into a single clipped row. + const flat = sanitizeTerminalText(String(raw ?? '')) + .replace(/\s+/g, ' ') + .trim() + .slice(0, MAX_OPTION_CHARS); + if (!flat || seen.has(flat)) continue; + seen.add(flat); + out.push(flat); + if (out.length >= MAX_OPTIONS) break; + } + return out; +} export function SelectList({ title, @@ -13,14 +34,16 @@ export function SelectList({ items: string[]; selectedIndex?: number; }) { - if (items.length === 0) return null; + const options = normalizeOptions(items); + if (options.length === 0) return null; + const safeIndex = Math.min(Math.max(0, selectedIndex), options.length - 1); return ( - {title} - {items.map((item, i) => { - const selected = i === selectedIndex; + {sanitizeTerminalText(title).slice(0, MAX_OPTION_CHARS)} + {options.map((item, i) => { + const selected = i === safeIndex; return ( - + {`${selected ? '❯ ' : ' '}${item}`} ); diff --git a/src/tui/eventCoalescer.test.ts b/src/tui/eventCoalescer.test.ts index 2a55374e..61e96739 100644 --- a/src/tui/eventCoalescer.test.ts +++ b/src/tui/eventCoalescer.test.ts @@ -79,4 +79,22 @@ describe('createCoalescer (INT-2407)', () => { vi.useRealTimers(); } }); + + it('drops oldest items once maxBatch is exceeded (AGT-3455)', () => { + vi.useFakeTimers(); + try { + const batches: number[][] = []; + const c = createCoalescer({ + delayMs: 50, + maxBatch: 3, + onFlush: (items) => batches.push(items), + }); + for (let i = 1; i <= 10; i++) c.push(i); + expect(c.pending()).toBe(3); + vi.advanceTimersByTime(50); + expect(batches).toEqual([[8, 9, 10]]); + } finally { + vi.useRealTimers(); + } + }); }); diff --git a/src/tui/eventCoalescer.ts b/src/tui/eventCoalescer.ts index b0026538..b523f246 100644 --- a/src/tui/eventCoalescer.ts +++ b/src/tui/eventCoalescer.ts @@ -25,6 +25,12 @@ export interface CoalescerOptions { onFlush: (items: T[]) => void; /** Max time an item waits before flushing. `<= 0` flushes synchronously on push. */ delayMs: number; + /** + * Hard cap on buffered items. Under an event flood, older items are dropped + * once this limit is reached so memory and a single React commit stay bounded. + * Default: unbounded (legacy behaviour). + */ + maxBatch?: number; /** Injectable timer (defaults to global setTimeout) — swapped in tests. */ setTimer?: (cb: () => void, ms: number) => TimerHandle; /** Injectable clear (defaults to global clearTimeout). */ @@ -39,6 +45,7 @@ export interface CoalescerOptions { export function createCoalescer(options: CoalescerOptions): Coalescer { const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms)); const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle)); + const maxBatch = options.maxBatch != null && options.maxBatch > 0 ? options.maxBatch : undefined; let buffer: T[] = []; let timer: TimerHandle | null = null; @@ -53,6 +60,10 @@ export function createCoalescer(options: CoalescerOptions): Coalescer { return { push(item) { buffer.push(item); + if (maxBatch != null && buffer.length > maxBatch) { + // Keep the newest events — older flood noise is less useful to render. + buffer = buffer.slice(-maxBatch); + } if (options.delayMs <= 0) { emit(); return; diff --git a/src/tui/hooks/usePipelineEvents.ts b/src/tui/hooks/usePipelineEvents.ts index 62271036..c2818f02 100644 --- a/src/tui/hooks/usePipelineEvents.ts +++ b/src/tui/hooks/usePipelineEvents.ts @@ -52,6 +52,7 @@ export function usePipelineEvents( // status stays immediate (not coalesced) so the live indicator is snappy. const coalescer = createCoalescer({ delayMs: flushMs, + maxBatch: 256, onFlush: (events) => { if (!disposed) dispatch({ type: 'batch', events }); }, diff --git a/src/tui/sse.test.ts b/src/tui/sse.test.ts index 4be34931..3ceb629e 100644 --- a/src/tui/sse.test.ts +++ b/src/tui/sse.test.ts @@ -57,12 +57,17 @@ describe('parseSseFrames (EPIC INT-1813 S5)', () => { it('reconnects with skipReplay after an established stream ends', async () => { const paths: string[] = []; const responses: EventEmitter[] = []; - httpGetMock.mockImplementation((options: { path: string }, callback: (res: EventEmitter & { setEncoding: () => void }) => void) => { + httpGetMock.mockImplementation((options: { path: string }, callback: (res: EventEmitter & { setEncoding: () => void; resume?: () => void }) => void) => { paths.push(options.path); - const res = Object.assign(new EventEmitter(), { setEncoding: vi.fn() }); + const res = Object.assign(new EventEmitter(), { + setEncoding: vi.fn(), + resume: vi.fn(), + statusCode: 200, + headers: { 'content-type': 'text/event-stream' }, + }); responses.push(res); callback(res); - return Object.assign(new EventEmitter(), { destroy: vi.fn() }); + return Object.assign(new EventEmitter(), { destroy: vi.fn(), setTimeout: vi.fn() }); }); const handle = connectEventStream({ port: 3847, onEvent: vi.fn(), reconnectMs: 1 }); @@ -72,4 +77,41 @@ describe('parseSseFrames (EPIC INT-1813 S5)', () => { expect(paths).toEqual(['/api/events', '/api/events?skipReplay=1']); }); + + it('reconnects when the request stalls before response headers (AGT-3455)', async () => { + const paths: string[] = []; + let firstReq: EventEmitter & { destroy: ReturnType; setTimeout: ReturnType }; + httpGetMock.mockImplementation((options: { path: string }, _callback: (res: EventEmitter) => void) => { + paths.push(options.path); + const req = Object.assign(new EventEmitter(), { + destroy: vi.fn(function (this: EventEmitter, err?: Error) { + this.emit('error', err ?? new Error('destroyed')); + }), + setTimeout: vi.fn(function (this: EventEmitter, ms: number) { + if (ms > 0 && paths.length === 1) { + queueMicrotask(() => this.emit('timeout')); + } + }), + }); + if (paths.length === 1) firstReq = req; + else { + const res = Object.assign(new EventEmitter(), { + setEncoding: vi.fn(), + resume: vi.fn(), + statusCode: 200, + headers: { 'content-type': 'text/event-stream' }, + }); + queueMicrotask(() => _callback(res)); + } + return req; + }); + + const handle = connectEventStream({ port: 3847, onEvent: vi.fn(), reconnectMs: 1 }); + await new Promise((resolve) => setTimeout(resolve, 20)); + handle.close(); + + expect(firstReq!.destroy).toHaveBeenCalled(); + expect(paths[0]).toBe('/api/events'); + expect(paths.length).toBeGreaterThanOrEqual(2); + }); }); diff --git a/src/tui/sse.ts b/src/tui/sse.ts index 738f0480..2c455431 100644 --- a/src/tui/sse.ts +++ b/src/tui/sse.ts @@ -9,6 +9,8 @@ import http from 'node:http'; import type { HubEvent } from '../core/eventHub.js'; const MAX_SSE_PARTIAL_BUFFER_CHARS = 64 * 1024; +/** Bound how long we wait for response headers before treating the connect as stalled. */ +const SSE_CONNECT_TIMEOUT_MS = 15_000; /** * Parse accumulated SSE text into events + the leftover (incomplete) tail. @@ -120,6 +122,8 @@ export function connectEventStream(opts: EventStreamOptions): EventStreamHandle req = http.get( { host, port: opts.port, path: eventStreamPath(connectedOnce), headers: { Accept: 'text/event-stream' } }, (res) => { + // Headers arrived — cancel the connect-stall timer. + req?.setTimeout(0); if (!isValidSseResponse(res)) { res.resume(); scheduleReconnect(); @@ -143,6 +147,12 @@ export function connectEventStream(opts: EventStreamOptions): EventStreamHandle res.on('end', scheduleReconnect); }, ); + // If the TCP connect succeeds but headers never arrive (stalled proxy/daemon), + // destroy and reconnect rather than hanging forever. (audit AGT-3455) + req.setTimeout(SSE_CONNECT_TIMEOUT_MS); + req.on('timeout', () => { + req?.destroy(new Error('SSE connect timed out waiting for response headers')); + }); req.on('error', scheduleReconnect); }