commit a909a99310f20f836f7b9d36238b6703e4987aa7 Author: centra Date: Sat Apr 4 14:59:45 2026 +0900 Initial agent-r orchestrator diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94b0f64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.agent-r +coverage +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..066f36b --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# agent-r + +`agent-r` is a local TypeScript CLI that orchestrates three Codex roles: + +- `strategy`: read-only planning, task breakdown, self-check +- `implementation`: workspace-write execution for one task at a time +- `checker`: read-only independent completion review + +The CLI persists state in SQLite plus run artifacts under `.agent-r/`, so a long-running effort can be resumed across invocations. + +## Commands + +```bash +npm install +npm run build + +agent-r run "Build a plugin system" --repo /path/to/repo +agent-r resume --repo /path/to/repo +agent-r status --repo /path/to/repo +agent-r inspect --repo /path/to/repo +agent-r logs --repo /path/to/repo --agent strategy +``` + +## Config + +Place `agent-r.config.toml` in the target repo if you want to override defaults. + +```toml +[state] +dir = ".agent-r" +max_task_attempts = 3 +max_cycles_per_run = 40 +max_replans = 12 +max_tasks = 200 +session_refresh_turns = 20 + +[codex] +command = "codex" + +[roles.strategy] +sandbox = "read-only" +search = true + +[roles.implementation] +sandbox = "workspace-write" +search = false + +[roles.checker] +sandbox = "read-only" +search = false +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c491a02 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2038 @@ +{ + "name": "agent-r", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-r", + "version": "0.1.0", + "dependencies": { + "@openai/codex-sdk": "^0.118.0", + "ajv": "^8.18.0", + "commander": "^14.0.3", + "smol-toml": "^1.6.1" + }, + "bin": { + "agent-r": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.1.2" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": 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/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "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/@openai/codex": { + "version": "0.118.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.118.0.tgz", + "integrity": "sha512-oJhNOsP/Vu7DBUhodpP15z60cCZv3sKORqUn1+pepleqSTmc0zXJZSjz6fQOLzny9Ra6sRcvprFFKmQ3Q6+E6w==", + "license": "Apache-2.0", + "bin": { + "codex": "bin/codex.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.118.0-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.118.0-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.118.0-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.118.0-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.118.0-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.118.0-win32-x64" + } + }, + "node_modules/@openai/codex-darwin-arm64": { + "name": "@openai/codex", + "version": "0.118.0-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.118.0-darwin-arm64.tgz", + "integrity": "sha512-sxq7Q2q9YiJN1wNrzvFRCC5oQKXPVUvu9Ejg6SI/CvyyI9YdtyLTO633wQJALGB7XQfT+3tyM5nNBivnSzLA3Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-darwin-x64": { + "name": "@openai/codex", + "version": "0.118.0-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.118.0-darwin-x64.tgz", + "integrity": "sha512-iIvrqzsh1iJmVfqyYwLZBiuh0MkN1Suqniz9hBfmRmE6txMWhRVfigb9SMBiOkfCW1C4sjBwLCsCSzg11rvppQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-arm64": { + "name": "@openai/codex", + "version": "0.118.0-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.118.0-linux-arm64.tgz", + "integrity": "sha512-YKWmIqLBu9mmBhN3JHQNkLzk0BtAtV6LBPvhnL5eeHkNrChvA6PKrhsjy0O7wvRkiByZY/1dD14qVdAUfxcbiA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-x64": { + "name": "@openai/codex", + "version": "0.118.0-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.118.0-linux-x64.tgz", + "integrity": "sha512-vzU2d/gwAmZbf/DnwVzfQiNN3Ri04XpaZiJkHElIvGoqFbdrxRQFYTtslGDISYIO3o+POADZcFZIfamFsZj9yQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-sdk": { + "version": "0.118.0", + "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.118.0.tgz", + "integrity": "sha512-cMisxx4CGQL44yv2USqdgcPhxPOhv227+CJiF9snn2X7nL+6wary4g38OknornjlrPob7LyzdIFB7dXqjkXKxg==", + "license": "Apache-2.0", + "dependencies": { + "@openai/codex": "0.118.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@openai/codex-win32-arm64": { + "name": "@openai/codex", + "version": "0.118.0-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.118.0-win32-arm64.tgz", + "integrity": "sha512-AU7GPVFqd0Ml3P2OXfy6K6TONcxfCZLUh1zjYkkoPGWJ4A3BVMkOmMP9CAdSiQsnU/RQfdV/9IGqo1xwUQDdjA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-win32-x64": { + "name": "@openai/codex", + "version": "0.118.0-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.118.0-win32-x64.tgz", + "integrity": "sha512-9vKW1ANQxIUsLpiY0VsOM+1avZjqxDZgqgxKABJHWRlLMuaSykwXexf8Hqq+BnYFfmn0d6MjxTuE1UtVvg9caw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "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.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "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/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "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/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.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.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "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", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..719b7c7 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "agent-r", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Autonomous multi-agent Codex orchestrator with strategy, implementation, and checking roles.", + "bin": { + "agent-r": "dist/index.js" + }, + "engines": { + "node": ">=24.0.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json && chmod +x dist/index.js", + "dev": "NODE_OPTIONS=--disable-warning=ExperimentalWarning tsx src/index.ts", + "lint": "tsc -p tsconfig.json --noEmit", + "test": "NODE_OPTIONS=--disable-warning=ExperimentalWarning vitest run" + }, + "dependencies": { + "@openai/codex-sdk": "^0.118.0", + "ajv": "^8.18.0", + "commander": "^14.0.3", + "smol-toml": "^1.6.1" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.1.2" + } +} diff --git a/src/artifacts.ts b/src/artifacts.ts new file mode 100644 index 0000000..c1579b7 --- /dev/null +++ b/src/artifacts.ts @@ -0,0 +1,69 @@ +import path from "node:path"; +import { ensureDir, safeSlug, timestampId, writeJsonFile, writeTextFile } from "./fs-utils.js"; +import type { AgentRole, InvocationArtifacts, RunArtifactsIndex } from "./types.js"; + +export class ArtifactManager { + constructor(private readonly runsDir: string) { + ensureDir(this.runsDir); + } + + getRunDir(runId: string): string { + const dir = path.join(this.runsDir, runId); + ensureDir(dir); + return dir; + } + + getRoleDir(runId: string, role: AgentRole): string { + const dir = path.join(this.getRunDir(runId), role); + ensureDir(dir); + return dir; + } + + createInvocationArtifacts(runId: string, role: AgentRole, schemaName: string): InvocationArtifacts { + const roleDir = this.getRoleDir(runId, role); + const baseName = `${timestampId()}-${safeSlug(schemaName)}`; + return { + promptPath: path.join(roleDir, `${baseName}.prompt.md`), + schemaPath: path.join(roleDir, `${baseName}.schema.json`), + rawEventsPath: path.join(roleDir, `${baseName}.events.jsonl`), + stderrPath: path.join(roleDir, `${baseName}.stderr.log`), + lastMessagePath: path.join(roleDir, `${baseName}.last.txt`), + responsePath: path.join(roleDir, `${baseName}.response.json`), + }; + } + + writePrompt(filePath: string, prompt: string): void { + writeTextFile(filePath, prompt); + } + + writeSchema(filePath: string, schema: Record): void { + writeJsonFile(filePath, schema); + } + + writeRawEvents(filePath: string, rawEvents: string[]): void { + writeTextFile(filePath, rawEvents.join("\n")); + } + + writeStderr(filePath: string, stderr: string): void { + writeTextFile(filePath, stderr); + } + + writeLastMessage(filePath: string, message: string): void { + writeTextFile(filePath, message); + } + + writeResponse(filePath: string, response: unknown): void { + writeJsonFile(filePath, response); + } + + indexFromArtifacts(artifacts: InvocationArtifacts): RunArtifactsIndex { + return { + promptPath: artifacts.promptPath, + responsePath: artifacts.responsePath, + schemaPath: artifacts.schemaPath, + rawEventsPath: artifacts.rawEventsPath, + stderrPath: artifacts.stderrPath, + lastMessagePath: artifacts.lastMessagePath, + }; + } +} diff --git a/src/codex-runner.ts b/src/codex-runner.ts new file mode 100644 index 0000000..f7d716c --- /dev/null +++ b/src/codex-runner.ts @@ -0,0 +1,139 @@ +import { Ajv } from "ajv"; +import { Codex, type ThreadEvent, type ThreadOptions } from "@openai/codex-sdk"; +import type { AgentInvocation, AgentInvocationResult, RawAgentRunner } from "./types.js"; + +function extractValidationError(message: string, errorText: string): string { + return [ + "The previous response did not parse or validate.", + `Problem: ${errorText}`, + "Respond again with JSON only, matching the schema exactly.", + "Previous response:", + message, + ].join("\n\n"); +} + +export class CodexSdkRunner implements RawAgentRunner { + private readonly ajv = new Ajv({ allErrors: true, strict: false }); + private readonly codex: Codex; + + constructor(codexCommand: string) { + this.codex = new Codex({ + codexPathOverride: codexCommand, + }); + } + + async invoke(request: AgentInvocation): Promise> { + let prompt = request.prompt; + let sessionId = request.sessionId; + const maxAttempts = request.maxValidationRetries ?? 2; + let lastFailure = ""; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const raw = await this.invokeOnce({ + ...request, + prompt, + sessionId, + }); + + sessionId = raw.sessionId; + + try { + const parsed = JSON.parse(raw.rawMessage) as unknown; + const validate = this.ajv.compile(request.schema); + if (!validate(parsed)) { + const errorText = this.ajv.errorsText(validate.errors); + lastFailure = errorText; + if (attempt === maxAttempts) { + throw new Error(`Schema validation failed: ${errorText}`); + } + prompt = extractValidationError(raw.rawMessage, errorText); + continue; + } + + return { + ...raw, + output: parsed as T, + }; + } catch (error) { + const errorText = error instanceof Error ? error.message : String(error); + lastFailure = errorText; + if (attempt === maxAttempts) { + throw new Error(`Structured output failed after ${attempt} attempt(s): ${lastFailure}`); + } + prompt = extractValidationError(raw.rawMessage, errorText); + } + } + + throw new Error(`Structured output failed: ${lastFailure}`); + } + + private async invokeOnce(request: AgentInvocation): Promise, "output">> { + const threadOptions: ThreadOptions = { + sandboxMode: request.roleConfig.sandbox, + workingDirectory: request.cwd, + skipGitRepoCheck: request.roleConfig.skipGitRepoCheck, + approvalPolicy: "never", + networkAccessEnabled: request.roleConfig.search, + webSearchEnabled: request.roleConfig.search, + webSearchMode: request.roleConfig.search ? "live" : "disabled", + }; + + if (request.roleConfig.model) { + threadOptions.model = request.roleConfig.model; + } + + const thread = request.sessionId + ? this.codex.resumeThread(request.sessionId, threadOptions) + : this.codex.startThread(threadOptions); + + const streamed = await thread.runStreamed(request.prompt, { + outputSchema: request.schema, + }); + + const rawEvents: string[] = []; + let sessionId = request.sessionId; + let rawMessage = ""; + + for await (const event of streamed.events) { + rawEvents.push(JSON.stringify(event)); + sessionId = this.extractSessionId(sessionId, event); + rawMessage = this.extractLatestMessage(rawMessage, event); + this.throwIfFailed(event); + } + + if (!rawMessage.trim()) { + throw new Error("Codex SDK completed without a final agent message."); + } + + return { + sessionId: sessionId ?? thread.id, + rawMessage, + rawEvents, + stderr: "", + artifacts: request.artifacts, + }; + } + + private extractSessionId(current: string | null, event: ThreadEvent): string | null { + if (event.type === "thread.started") { + return event.thread_id; + } + return current; + } + + private extractLatestMessage(current: string, event: ThreadEvent): string { + if (event.type === "item.completed" && event.item.type === "agent_message") { + return event.item.text; + } + return current; + } + + private throwIfFailed(event: ThreadEvent): void { + if (event.type === "error") { + throw new Error(event.message); + } + if (event.type === "turn.failed") { + throw new Error(event.error.message); + } + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..f0c4f2b --- /dev/null +++ b/src/config.ts @@ -0,0 +1,138 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { parse } from "smol-toml"; +import type { AgentRole, ResolvedConfig, RoleConfig } from "./types.js"; + +interface ConfigRoleOverride { + model?: string; + sandbox?: RoleConfig["sandbox"]; + search?: boolean; + skipGitRepoCheck?: boolean; + skip_git_repo_check?: boolean; + extraArgs?: string[]; + extra_args?: string[]; +} + +interface ConfigFileShape { + state?: { + dir?: string; + max_task_attempts?: number; + max_cycles_per_run?: number; + max_replans?: number; + max_tasks?: number; + session_refresh_turns?: number; + }; + codex?: { + command?: string; + }; + roles?: Partial>; +} + +function defaultRoleConfig(role: AgentRole): RoleConfig { + if (role === "strategy") { + return { + sandbox: "read-only", + search: true, + skipGitRepoCheck: false, + extraArgs: [], + }; + } + + if (role === "checker") { + return { + sandbox: "read-only", + search: false, + skipGitRepoCheck: false, + extraArgs: [], + }; + } + + return { + sandbox: "workspace-write", + search: false, + skipGitRepoCheck: false, + extraArgs: [], + }; +} + +function mergeRoleConfig(role: AgentRole, override?: Partial): RoleConfig { + const base = defaultRoleConfig(role); + const merged: RoleConfig = { + sandbox: override?.sandbox ?? base.sandbox, + search: override?.search ?? base.search, + skipGitRepoCheck: override?.skipGitRepoCheck ?? base.skipGitRepoCheck, + extraArgs: override?.extraArgs ?? base.extraArgs, + }; + + const model = override?.model ?? base.model; + if (model) { + merged.model = model; + } + + return merged; +} + +export function loadConfig(repoPath: string, configPath?: string): ResolvedConfig { + const absoluteRepoPath = path.resolve(repoPath); + const effectiveConfigPath = configPath ? path.resolve(configPath) : path.join(absoluteRepoPath, "agent-r.config.toml"); + let parsedConfig: ConfigFileShape = {}; + + if (existsSync(effectiveConfigPath)) { + parsedConfig = parse(readFileSync(effectiveConfigPath, "utf8")) as ConfigFileShape; + } + + const stateDir = path.resolve( + absoluteRepoPath, + parsedConfig.state?.dir ?? ".agent-r", + ); + + return { + repoPath: absoluteRepoPath, + stateDir, + databasePath: path.join(stateDir, "state.sqlite"), + runsDir: path.join(stateDir, "runs"), + codexCommand: parsedConfig.codex?.command ?? "codex", + maxTaskAttempts: parsedConfig.state?.max_task_attempts ?? 3, + maxCyclesPerInvocation: parsedConfig.state?.max_cycles_per_run ?? 40, + maxReplans: parsedConfig.state?.max_replans ?? 12, + maxTasks: parsedConfig.state?.max_tasks ?? 200, + sessionRefreshTurns: parsedConfig.state?.session_refresh_turns ?? 20, + roles: { + strategy: mergeRoleConfig("strategy", normalizeRoleOverride(parsedConfig.roles?.strategy)), + implementation: mergeRoleConfig("implementation", normalizeRoleOverride(parsedConfig.roles?.implementation)), + checker: mergeRoleConfig("checker", normalizeRoleOverride(parsedConfig.roles?.checker)), + }, + }; +} + +function normalizeRoleOverride(override?: ConfigRoleOverride): Partial | undefined { + if (!override) { + return undefined; + } + + const normalized: Partial = {}; + + if (override.model) { + normalized.model = override.model; + } + + if (override.sandbox) { + normalized.sandbox = override.sandbox; + } + + if (override.search !== undefined) { + normalized.search = override.search; + } + + const skipGitRepoCheck = override.skipGitRepoCheck ?? override.skip_git_repo_check; + if (skipGitRepoCheck !== undefined) { + normalized.skipGitRepoCheck = skipGitRepoCheck; + } + + const extraArgs = override.extraArgs ?? override.extra_args; + if (extraArgs !== undefined) { + normalized.extraArgs = extraArgs; + } + + return normalized; +} diff --git a/src/fs-utils.ts b/src/fs-utils.ts new file mode 100644 index 0000000..644694f --- /dev/null +++ b/src/fs-utils.ts @@ -0,0 +1,42 @@ +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +export function ensureDir(dirPath: string): void { + mkdirSync(dirPath, { recursive: true }); +} + +export function writeTextFile(filePath: string, text: string): void { + ensureDir(path.dirname(filePath)); + writeFileSync(filePath, text, "utf8"); +} + +export function writeJsonFile(filePath: string, value: unknown): void { + writeTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +export function readTextFile(filePath: string): string { + return readFileSync(filePath, "utf8"); +} + +export function safeSlug(value: string): string { + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 40) || "artifact"; +} + +export function timestampId(date = new Date()): string { + return date.toISOString().replace(/[:.]/g, "-"); +} + +export function listFilesRecursive(dirPath: string): string[] { + const entries = readdirSync(dirPath, { withFileTypes: true }); + return entries.flatMap((entry) => { + const fullPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + return listFilesRecursive(fullPath); + } + return [fullPath]; + }); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..fb9035d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,127 @@ +#!/usr/bin/env -S node --disable-warning=ExperimentalWarning +import path from "node:path"; +import { Command } from "commander"; +import { ArtifactManager } from "./artifacts.js"; +import { CodexSdkRunner } from "./codex-runner.js"; +import { loadConfig } from "./config.js"; +import { listFilesRecursive, readTextFile } from "./fs-utils.js"; +import { AgentROrchestrator } from "./orchestrator.js"; +import { RunStore } from "./store.js"; +import type { AgentRole } from "./types.js"; + +function resolveRepoPath(repo?: string): string { + return path.resolve(repo ?? process.cwd()); +} + +function bootstrap(repo?: string, configPath?: string) { + const repoPath = resolveRepoPath(repo); + const config = loadConfig(repoPath, configPath); + const store = new RunStore(config.databasePath); + const artifacts = new ArtifactManager(config.runsDir); + const runner = new CodexSdkRunner(config.codexCommand); + const orchestrator = new AgentROrchestrator(config, store, artifacts, runner); + return { repoPath, config, store, artifacts, orchestrator }; +} + +function printRunStatus(label: string, run: { + id: string; + status: string; + summary: string | null; + goal: string; + cycleCount: number; + replanCount: number; + currentTaskId: string | null; +}): void { + console.log(`${label}: ${run.id}`); + console.log(`status: ${run.status}`); + console.log(`goal: ${run.goal}`); + console.log(`summary: ${run.summary ?? "none"}`); + console.log(`cycles: ${run.cycleCount}`); + console.log(`replans: ${run.replanCount}`); + console.log(`current task: ${run.currentTaskId ?? "none"}`); +} + +const program = new Command(); + +program.name("agent-r").description("Autonomous Codex multi-agent orchestrator."); + +program + .command("run") + .argument("", "high-level development goal") + .option("--repo ", "target repository path") + .option("--config ", "path to agent-r config file") + .option("--max-cycles ", "override max cycles for this invocation", Number) + .action(async (goal: string, options: { repo?: string; config?: string; maxCycles?: number }) => { + const { orchestrator, store } = bootstrap(options.repo, options.config); + const run = orchestrator.createRun(goal); + const finalRun = await orchestrator.runUntilStable(run.id, options.maxCycles); + printRunStatus("run", finalRun); + console.log(`run id: ${run.id}`); + console.log(`pending tasks: ${store.buildSnapshot(run.id).pendingTasks.length}`); + }); + +program + .command("resume") + .argument("", "run id") + .option("--repo ", "target repository path") + .option("--config ", "path to agent-r config file") + .option("--max-cycles ", "override max cycles for this invocation", Number) + .action(async (runId: string, options: { repo?: string; config?: string; maxCycles?: number }) => { + const { orchestrator } = bootstrap(options.repo, options.config); + const finalRun = await orchestrator.runUntilStable(runId, options.maxCycles); + printRunStatus("run", finalRun); + }); + +program + .command("status") + .argument("", "run id") + .option("--repo ", "target repository path") + .option("--config ", "path to agent-r config file") + .action((runId: string, options: { repo?: string; config?: string }) => { + const { store } = bootstrap(options.repo, options.config); + const snapshot = store.buildSnapshot(runId); + printRunStatus("run", snapshot.run); + console.log(`pending: ${snapshot.pendingTasks.length}`); + console.log(`completed: ${snapshot.completedTasks.length}`); + console.log(`blocked/abandoned: ${snapshot.blockedTasks.length}`); + }); + +program + .command("inspect") + .argument("", "run id") + .option("--repo ", "target repository path") + .option("--config ", "path to agent-r config file") + .action((runId: string, options: { repo?: string; config?: string }) => { + const { store } = bootstrap(options.repo, options.config); + console.log(JSON.stringify(store.buildSnapshot(runId), null, 2)); + }); + +program + .command("logs") + .argument("", "run id") + .option("--repo ", "target repository path") + .option("--config ", "path to agent-r config file") + .option("--agent ", "limit to a single role") + .action((runId: string, options: { repo?: string; config?: string; agent?: AgentRole }) => { + const { artifacts, store } = bootstrap(options.repo, options.config); + const snapshot = store.buildSnapshot(runId); + const artifactFiles = options.agent + ? store.listArtifacts(runId, options.agent).map((artifact) => artifact.path) + : listFilesRecursive(artifacts.getRunDir(runId)); + + console.log(`run: ${runId}`); + console.log(`status: ${snapshot.run.status}`); + console.log(""); + + for (const filePath of artifactFiles) { + console.log(`# ${filePath}`); + console.log(readTextFile(filePath)); + console.log(""); + } + }); + +program.parseAsync(process.argv).catch((error: unknown) => { + const message = error instanceof Error ? error.stack ?? error.message : String(error); + console.error(message); + process.exitCode = 1; +}); diff --git a/src/orchestrator.ts b/src/orchestrator.ts new file mode 100644 index 0000000..186dd2b --- /dev/null +++ b/src/orchestrator.ts @@ -0,0 +1,423 @@ +import type { + AgentRole, + AgentStateRecord, + CheckVerdictOutput, + ImplementationResultOutput, + RawAgentRunner, + ResolvedConfig, + RunRecord, + RunSnapshot, + StrategyPlanOutput, + StrategySelfCheckOutput, + TaskDraft, + TaskRecord, +} from "./types.js"; +import { ArtifactManager } from "./artifacts.js"; +import { + buildCheckPrompt, + buildImplementationPrompt, + buildStrategyPlanPrompt, + buildStrategySelfCheckPrompt, + formatCheckVerdictSummary, + formatImplementationSummary, + formatSelfCheckSummary, + formatStrategyPlanSummary, +} from "./prompts.js"; +import { checkVerdictSchema, implementationResultSchema, strategyPlanSchema, strategySelfCheckSchema } from "./schema-catalog.js"; +import { RunStore } from "./store.js"; + +function nowIso(): string { + return new Date().toISOString(); +} + +function blockerSignature(blockers: string[]): string | null { + const compact = blockers.map((value) => value.trim().toLowerCase()).filter(Boolean).sort().join("|"); + return compact || null; +} + +function uniqueTaskDrafts(tasks: TaskDraft[]): TaskDraft[] { + const seen = new Set(); + const result: TaskDraft[] = []; + for (const task of tasks) { + const key = JSON.stringify(task); + if (seen.has(key)) { + continue; + } + seen.add(key); + result.push(task); + } + return result; +} + +export class AgentROrchestrator { + constructor( + private readonly config: ResolvedConfig, + private readonly store: RunStore, + private readonly artifacts: ArtifactManager, + private readonly runner: RawAgentRunner, + ) {} + + createRun(goal: string): RunRecord { + return this.store.createRun(goal, this.config.repoPath); + } + + async runUntilStable(runId: string, maxCycles = this.config.maxCyclesPerInvocation): Promise { + this.store.requeueInProgressTasks(runId); + + for (let cycle = 0; cycle < maxCycles; cycle += 1) { + const snapshot = this.store.buildSnapshot(runId); + if (snapshot.run.status === "done" || snapshot.run.status === "blocked") { + return snapshot.run; + } + + this.store.incrementCycle(runId); + + switch (snapshot.run.status) { + case "planning": + if (snapshot.pendingTasks.length > 0) { + await this.dispatchNextTask(runId); + } else { + await this.runStrategyPlan(runId); + } + break; + case "implementing": + await this.dispatchNextTask(runId); + break; + case "self_check": + await this.runStrategySelfCheck(runId); + break; + case "independent_check": + await this.runIndependentCheck(runId); + break; + default: + throw new Error(`Unsupported run state: ${snapshot.run.status}`); + } + } + + this.store.addEvent(runId, "system", "cycle_budget_exhausted", "Reached max cycles for this invocation.", { maxCycles }); + return this.store.getRun(runId); + } + + private async runStrategyPlan(runId: string): Promise { + const snapshot = this.store.buildSnapshot(runId); + const response = await this.invokeRole({ + runId, + role: "strategy", + schemaName: "strategy-plan", + prompt: buildStrategyPlanPrompt(snapshot), + schema: strategyPlanSchema, + }); + + const plan = response.output; + this.store.addEvent(runId, "strategy", "strategy_plan", formatStrategyPlanSummary(plan), plan); + this.store.updateRun(runId, { summary: plan.summary, currentTaskId: null }); + this.store.incrementReplanCount(runId); + + if (this.store.getRun(runId).replanCount > this.config.maxReplans) { + this.store.updateRun(runId, { + status: "blocked", + summary: `Exceeded replan limit (${this.config.maxReplans}).`, + currentTaskId: null, + }); + this.store.addEvent(runId, "system", "blocked", "Run blocked because the replan limit was exceeded.", {}); + return; + } + + if (plan.decision === "blocked") { + this.store.updateRun(runId, { + status: "blocked", + summary: plan.blockedReason ?? plan.summary, + currentTaskId: null, + }); + this.store.addEvent(runId, "strategy", "blocked", "Strategy declared the run blocked.", plan); + this.checkpoint(runId); + return; + } + + if (plan.decision === "done") { + this.store.updateRun(runId, { + status: "self_check", + summary: plan.summary, + currentTaskId: null, + }); + this.checkpoint(runId); + return; + } + + const tasks = uniqueTaskDrafts(plan.tasks).slice(0, this.config.maxTasks); + if (!tasks.length) { + this.store.updateRun(runId, { + status: "blocked", + summary: "Strategy returned continue without any tasks.", + currentTaskId: null, + }); + this.store.addEvent(runId, "system", "blocked", "Run blocked because no tasks were returned.", plan); + this.checkpoint(runId); + return; + } + + this.store.replacePendingTasks(runId, tasks); + this.store.updateRun(runId, { + status: "planning", + summary: plan.summary, + currentTaskId: null, + }); + this.checkpoint(runId); + } + + private async dispatchNextTask(runId: string): Promise { + const claimedTask = this.store.claimNextPendingTask(runId); + if (!claimedTask) { + this.store.updateRun(runId, { status: "planning", currentTaskId: null }); + return; + } + + this.store.updateRun(runId, { + status: "implementing", + currentTaskId: claimedTask.id, + summary: `Implementing ${claimedTask.title}`, + }); + this.store.addEvent(runId, "system", "task_dispatched", `Dispatched task ${claimedTask.id}.`, { taskId: claimedTask.id }); + + const snapshot = this.store.buildSnapshot(runId); + const response = await this.invokeRole({ + runId, + role: "implementation", + schemaName: "implementation-result", + prompt: buildImplementationPrompt(snapshot, claimedTask), + schema: implementationResultSchema, + }); + + const result = response.output; + if (result.taskId !== claimedTask.id) { + throw new Error(`Implementation agent returned mismatched task id: expected ${claimedTask.id}, got ${result.taskId}`); + } + + const signature = blockerSignature(result.blockers); + const previousAttempt = this.store.latestTaskAttempt(claimedTask.id); + const latestTask = this.store.getTask(claimedTask.id); + + this.store.addTaskAttempt( + runId, + claimedTask.id, + latestTask.attemptCount, + result.status, + result.summary, + result, + signature, + ); + this.store.addEvent(runId, "implementation", "implementation_result", formatImplementationSummary(result), result); + + if (result.status === "completed") { + this.store.completeTask(claimedTask.id, result.summary); + this.store.updateRun(runId, { + status: "planning", + currentTaskId: null, + summary: result.summary, + }); + this.checkpoint(runId); + return; + } + + const repeatedBlocker = + previousAttempt && + previousAttempt.blockerSignature && + previousAttempt.blockerSignature === signature; + + if (result.status === "blocked") { + this.store.blockTask(claimedTask.id, result.summary, signature); + if (repeatedBlocker) { + this.store.updateRun(runId, { + status: "blocked", + currentTaskId: null, + summary: `Repeated blocker for task ${claimedTask.title}.`, + }); + this.store.addEvent(runId, "system", "blocked", "Run blocked because the same blocker repeated.", { + taskId: claimedTask.id, + blockerSignature: signature, + }); + this.checkpoint(runId); + return; + } + this.store.updateRun(runId, { + status: "planning", + currentTaskId: null, + summary: result.summary, + }); + this.checkpoint(runId); + return; + } + + if (latestTask.attemptCount >= this.config.maxTaskAttempts) { + this.store.blockTask(claimedTask.id, result.summary, signature); + this.store.updateRun(runId, { + status: "blocked", + currentTaskId: null, + summary: `Task ${claimedTask.title} exceeded the retry limit.`, + }); + this.store.addEvent(runId, "system", "blocked", "Run blocked because task retry limit was exceeded.", { + taskId: claimedTask.id, + attempts: latestTask.attemptCount, + }); + this.checkpoint(runId); + return; + } + + this.store.abandonTask(claimedTask.id, result.summary, signature); + this.store.updateRun(runId, { + status: "planning", + currentTaskId: null, + summary: result.summary, + }); + this.checkpoint(runId); + } + + private async runStrategySelfCheck(runId: string): Promise { + const snapshot = this.store.buildSnapshot(runId); + const lastPlan = snapshot.recentEvents.findLast((event) => event.kind === "strategy_plan")?.payload as + | StrategyPlanOutput + | undefined; + if (!lastPlan) { + this.store.updateRun(runId, { status: "planning" }); + return; + } + + const response = await this.invokeRole({ + runId, + role: "strategy", + schemaName: "strategy-self-check", + prompt: buildStrategySelfCheckPrompt(snapshot, lastPlan), + schema: strategySelfCheckSchema, + }); + + const selfCheck = response.output; + this.store.addApproval( + runId, + "strategy_self_check", + selfCheck.readyForIndependentCheck ? "approved" : "rejected", + selfCheck.rationale, + selfCheck, + ); + this.store.addEvent(runId, "strategy", "strategy_self_check", formatSelfCheckSummary(selfCheck), selfCheck); + + this.store.updateRun(runId, { + status: selfCheck.readyForIndependentCheck ? "independent_check" : "planning", + summary: selfCheck.summary, + currentTaskId: null, + }); + this.checkpoint(runId); + } + + private async runIndependentCheck(runId: string): Promise { + const snapshot = this.store.buildSnapshot(runId); + const latestSelfCheck = this.store.latestApproval(runId, "strategy_self_check")?.payload as + | StrategySelfCheckOutput + | undefined; + + if (!latestSelfCheck) { + this.store.updateRun(runId, { status: "planning" }); + return; + } + + const response = await this.invokeRole({ + runId, + role: "checker", + schemaName: "independent-check", + prompt: buildCheckPrompt(snapshot, latestSelfCheck), + schema: checkVerdictSchema, + }); + + const verdict = response.output; + this.store.addApproval( + runId, + "checker", + verdict.verdict === "approved" ? "approved" : "rejected", + verdict.rationale, + verdict, + ); + this.store.addEvent(runId, "checker", "check_verdict", formatCheckVerdictSummary(verdict), verdict); + + if (verdict.verdict === "approved") { + this.store.updateRun(runId, { + status: "done", + summary: verdict.summary, + currentTaskId: null, + }); + this.checkpoint(runId); + return; + } + + this.store.updateRun(runId, { + status: "planning", + summary: verdict.summary, + currentTaskId: null, + }); + this.checkpoint(runId); + } + + private async invokeRole(options: { + runId: string; + role: AgentRole; + schemaName: string; + prompt: string; + schema: Record; + }): Promise<{ output: T }> { + const state = this.store.getAgentState(options.runId, options.role); + const nextSession = this.shouldRotate(state) ? null : state?.sessionId ?? null; + const prompt = this.shouldRotate(state) + ? `Session refresh for role ${options.role}. Continue the run using this compressed state.\n\n${options.prompt}` + : options.prompt; + const artifacts = this.artifacts.createInvocationArtifacts(options.runId, options.role, options.schemaName); + + this.artifacts.writePrompt(artifacts.promptPath, prompt); + this.artifacts.writeSchema(artifacts.schemaPath, options.schema); + + const result = await this.runner.invoke({ + runId: options.runId, + role: options.role, + sessionId: nextSession, + prompt, + schemaName: options.schemaName, + schema: options.schema, + cwd: this.config.repoPath, + roleConfig: this.config.roles[options.role], + artifacts, + maxValidationRetries: 2, + }); + + this.artifacts.writeRawEvents(artifacts.rawEventsPath, result.rawEvents); + this.artifacts.writeStderr(artifacts.stderrPath, result.stderr); + this.artifacts.writeLastMessage(artifacts.lastMessagePath, result.rawMessage); + this.artifacts.writeResponse(artifacts.responsePath, result.output); + + this.store.addArtifact(options.runId, options.role, `${options.schemaName}.prompt`, artifacts.promptPath, {}); + this.store.addArtifact(options.runId, options.role, `${options.schemaName}.schema`, artifacts.schemaPath, {}); + this.store.addArtifact(options.runId, options.role, `${options.schemaName}.events`, artifacts.rawEventsPath, {}); + this.store.addArtifact(options.runId, options.role, `${options.schemaName}.stderr`, artifacts.stderrPath, {}); + this.store.addArtifact(options.runId, options.role, `${options.schemaName}.last`, artifacts.lastMessagePath, {}); + this.store.addArtifact(options.runId, options.role, `${options.schemaName}.response`, artifacts.responsePath, {}); + + const nextState: AgentStateRecord = { + runId: options.runId, + role: options.role, + sessionId: result.sessionId, + turns: (this.shouldRotate(state) ? 0 : state?.turns ?? 0) + 1, + rotationCount: state ? state.rotationCount + (this.shouldRotate(state) ? 1 : 0) : 0, + lastPromptPath: artifacts.promptPath, + lastResponsePath: artifacts.responsePath, + updatedAt: nowIso(), + }; + this.store.saveAgentState(nextState); + + return { output: result.output }; + } + + private shouldRotate(state: AgentStateRecord | null): boolean { + return Boolean(state?.sessionId && state.turns >= this.config.sessionRefreshTurns); + } + + private checkpoint(runId: string): void { + const snapshot = this.store.buildSnapshot(runId); + this.store.addCheckpoint(runId, snapshot.run.status, snapshot); + } +} diff --git a/src/prompts.ts b/src/prompts.ts new file mode 100644 index 0000000..12fb572 --- /dev/null +++ b/src/prompts.ts @@ -0,0 +1,250 @@ +import type { + CheckVerdictOutput, + RunSnapshot, + StrategyPlanOutput, + StrategySelfCheckOutput, + TaskDraft, + TaskRecord, +} from "./types.js"; + +function bulletList(values: string[]): string { + return values.length ? values.map((value) => `- ${value}`).join("\n") : "- none"; +} + +function renderTask(task: TaskDraft | TaskRecord): string { + return [ + `Title: ${task.title}`, + `Objective: ${task.objective}`, + `Acceptance:`, + bulletList(task.acceptanceCriteria), + `Verification:`, + bulletList(task.verificationSteps), + `Allowed paths:`, + bulletList(task.allowedPaths), + ].join("\n"); +} + +function renderTasks(tasks: TaskDraft[] | TaskRecord[]): string { + return tasks.length + ? tasks.map((task, index) => `Task ${index + 1}\n${renderTask(task)}`).join("\n\n") + : "No tasks."; +} + +function renderApprovals(snapshot: RunSnapshot): string { + if (!snapshot.approvals.length) { + return "No approvals yet."; + } + + return snapshot.approvals + .map( + (approval) => + `- ${approval.createdAt} ${approval.source} ${approval.verdict}: ${approval.rationale}`, + ) + .join("\n"); +} + +function renderRecentAttempts(snapshot: RunSnapshot): string { + if (!snapshot.recentAttempts.length) { + return "No implementation attempts yet."; + } + + return snapshot.recentAttempts + .map((attempt) => { + const payload = JSON.parse(attempt.resultJson) as { changes?: string[]; followUps?: string[]; blockers?: string[] }; + return [ + `- Task ${attempt.taskId} attempt ${attempt.attemptNumber}: ${attempt.status}`, + ` Summary: ${attempt.summary}`, + ` Changes: ${(payload.changes ?? []).join("; ") || "none"}`, + ` Follow-ups: ${(payload.followUps ?? []).join("; ") || "none"}`, + ` Blockers: ${(payload.blockers ?? []).join("; ") || "none"}`, + ].join("\n"); + }) + .join("\n"); +} + +function renderEvents(snapshot: RunSnapshot): string { + if (!snapshot.recentEvents.length) { + return "No events yet."; + } + + return snapshot.recentEvents + .map((event) => `- ${event.ts} ${event.kind}: ${event.message}`) + .join("\n"); +} + +function renderRunState(snapshot: RunSnapshot): string { + return [ + `Run ID: ${snapshot.run.id}`, + `Goal: ${snapshot.run.goal}`, + `Status: ${snapshot.run.status}`, + `Repo: ${snapshot.run.repoPath}`, + `Cycles executed: ${snapshot.run.cycleCount}`, + `Replans: ${snapshot.run.replanCount}`, + `Current task: ${snapshot.run.currentTaskId ?? "none"}`, + ].join("\n"); +} + +export function buildStrategyPlanPrompt(snapshot: RunSnapshot): string { + const checkerApproval = snapshot.approvals.findLast((approval) => approval.source === "checker"); + return ` +You are the strategy agent for an autonomous development system. + +Role rules: +- You may inspect the repository directly, but treat that as research only. +- Do not edit files and do not implement tasks yourself. +- Break work into concrete tasks for the implementation agent. +- Lean on the provided summary first. Read more of the repo only when it materially improves strategy. +- Only declare the project done when the user goal is actually satisfied, with evidence from completed work. + +Return JSON that matches the provided schema exactly. + +Current run state: +${renderRunState(snapshot)} + +Pending tasks: +${renderTasks(snapshot.pendingTasks)} + +Completed tasks: +${renderTasks(snapshot.completedTasks)} + +Blocked or abandoned tasks: +${renderTasks(snapshot.blockedTasks)} + +Recent implementation attempts: +${renderRecentAttempts(snapshot)} + +Recent events: +${renderEvents(snapshot)} + +Latest approvals: +${renderApprovals(snapshot)} + +Latest checker verdict: +${checkerApproval ? JSON.stringify(checkerApproval.payload, null, 2) : "No checker verdict yet."} + +Decision policy: +- Use \`decision: "continue"\` when there is meaningful work left. Provide the complete next backlog. +- Use \`decision: "done"\` only when no substantial work remains. +- Use \`decision: "blocked"\` only for external blockers or unresolved contradictions. +- Keep the backlog concise. Prefer a few substantive tasks over many trivial tasks. + `.trim(); +} + +export function buildImplementationPrompt(snapshot: RunSnapshot, task: TaskRecord): string { + return ` +You are the implementation agent for an autonomous development system. + +Role rules: +- You are responsible for executing exactly one task. +- You may modify files in the repository. +- Stay within the task's allowed paths unless the task itself truly requires broader changes, and call that out if it happens. +- Run relevant verification when possible. +- If you cannot complete the task, return \`needs_replan\` or \`blocked\` with concrete blockers. + +Return JSON that matches the provided schema exactly. + +Overall goal: +${snapshot.run.goal} + +Task: +${renderTask(task)} + +Recent completed tasks: +${renderTasks(snapshot.completedTasks.slice(-5))} + +Recent blocked tasks: +${renderTasks(snapshot.blockedTasks.slice(-5))} + +Recent approvals: +${renderApprovals(snapshot)} + +You must echo the exact task id \`${task.id}\`. + `.trim(); +} + +export function buildStrategySelfCheckPrompt( + snapshot: RunSnapshot, + lastPlan: StrategyPlanOutput, +): string { + return ` +You are the strategy agent performing a self-check before independent review. + +Role rules: +- Re-evaluate the original goal against the repository and the execution history. +- You may inspect the repo, but you still must not edit files. +- Be strict. If meaningful work remains, do not send the run to the checker. + +Return JSON that matches the provided schema exactly. + +Original goal: +${snapshot.run.goal} + +Latest strategy decision: +${JSON.stringify(lastPlan, null, 2)} + +Completed tasks: +${renderTasks(snapshot.completedTasks)} + +Blocked or abandoned tasks: +${renderTasks(snapshot.blockedTasks)} + +Recent implementation attempts: +${renderRecentAttempts(snapshot)} + `.trim(); +} + +export function buildCheckPrompt( + snapshot: RunSnapshot, + selfCheck: StrategySelfCheckOutput, +): string { + return ` +You are the independent check agent for an autonomous development system. + +Role rules: +- You are independent from the strategy agent. +- You may inspect the repository directly. +- You must not edit files. +- Evaluate whether the original user goal is actually complete. +- Reject completion when there are still meaningful tasks, risks, or missing evidence. + +Return JSON that matches the provided schema exactly. + +Original goal: +${snapshot.run.goal} + +Current run state: +${renderRunState(snapshot)} + +Strategy self-check: +${JSON.stringify(selfCheck, null, 2)} + +Completed tasks: +${renderTasks(snapshot.completedTasks)} + +Blocked or abandoned tasks: +${renderTasks(snapshot.blockedTasks)} + +Recent implementation attempts: +${renderRecentAttempts(snapshot)} + `.trim(); +} + +export function formatStrategyPlanSummary(plan: StrategyPlanOutput): string { + return `${plan.decision}: ${plan.summary}`; +} + +export function formatImplementationSummary(result: { + status: string; + summary: string; + touchedFiles: string[]; +}): string { + return `${result.status}: ${result.summary} | touched: ${result.touchedFiles.join(", ") || "none"}`; +} + +export function formatSelfCheckSummary(result: StrategySelfCheckOutput): string { + return `${result.readyForIndependentCheck ? "ready" : "not ready"}: ${result.summary}`; +} + +export function formatCheckVerdictSummary(result: CheckVerdictOutput): string { + return `${result.verdict}: ${result.summary}`; +} diff --git a/src/schema-catalog.ts b/src/schema-catalog.ts new file mode 100644 index 0000000..f82b43b --- /dev/null +++ b/src/schema-catalog.ts @@ -0,0 +1,141 @@ +export const taskDraftSchema = { + type: "object", + additionalProperties: false, + required: ["title", "objective", "acceptanceCriteria", "verificationSteps", "allowedPaths"], + properties: { + title: { type: "string", minLength: 1 }, + objective: { type: "string", minLength: 1 }, + acceptanceCriteria: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + verificationSteps: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + allowedPaths: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + }, +} as const; + +export const strategyPlanSchema = { + type: "object", + additionalProperties: false, + required: ["decision", "summary", "rationale", "goalProgress", "risks", "tasks"], + properties: { + decision: { + type: "string", + enum: ["continue", "done", "blocked"], + }, + summary: { type: "string", minLength: 1 }, + rationale: { type: "string", minLength: 1 }, + goalProgress: { type: "string", minLength: 1 }, + risks: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + tasks: { + type: "array", + items: taskDraftSchema, + }, + blockedReason: { type: "string" }, + }, +} as const; + +export const strategySelfCheckSchema = { + type: "object", + additionalProperties: false, + required: ["readyForIndependentCheck", "summary", "rationale", "evidence", "remainingGaps"], + properties: { + readyForIndependentCheck: { type: "boolean" }, + summary: { type: "string", minLength: 1 }, + rationale: { type: "string", minLength: 1 }, + evidence: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + remainingGaps: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + }, +} as const; + +export const implementationResultSchema = { + type: "object", + additionalProperties: false, + required: [ + "taskId", + "status", + "summary", + "changes", + "verification", + "followUps", + "touchedFiles", + "blockers", + ], + properties: { + taskId: { type: "string", minLength: 1 }, + status: { + type: "string", + enum: ["completed", "needs_replan", "blocked"], + }, + summary: { type: "string", minLength: 1 }, + changes: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + verification: { + type: "array", + items: { + type: "object", + additionalProperties: false, + required: ["command", "outcome", "details"], + properties: { + command: { type: "string", minLength: 1 }, + outcome: { + type: "string", + enum: ["passed", "failed", "not_run"], + }, + details: { type: "string", minLength: 1 }, + }, + }, + }, + followUps: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + touchedFiles: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + blockers: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + }, +} as const; + +export const checkVerdictSchema = { + type: "object", + additionalProperties: false, + required: ["verdict", "summary", "rationale", "evidence", "remainingTasks"], + properties: { + verdict: { + type: "string", + enum: ["approved", "rejected"], + }, + summary: { type: "string", minLength: 1 }, + rationale: { type: "string", minLength: 1 }, + evidence: { + type: "array", + items: { type: "string", minLength: 1 }, + }, + remainingTasks: { + type: "array", + items: taskDraftSchema, + }, + }, +} as const; diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..d169406 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,647 @@ +import { randomUUID } from "node:crypto"; +import { DatabaseSync } from "node:sqlite"; +import path from "node:path"; +import { ensureDir } from "./fs-utils.js"; +import type { + AgentRole, + AgentStateRecord, + ApprovalRecord, + ApprovalSource, + ApprovalVerdict, + ArtifactRecord, + AttemptStatus, + CheckpointRecord, + EventRecord, + RunRecord, + RunSnapshot, + RunStatus, + TaskAttemptRecord, + TaskDraft, + TaskRecord, + TaskStatus, +} from "./types.js"; + +function nowIso(): string { + return new Date().toISOString(); +} + +function parseJson(value: string | null): T { + if (!value) { + return [] as T; + } + return JSON.parse(value) as T; +} + +function toJson(value: unknown): string { + return JSON.stringify(value); +} + +function mapTaskRow(row: Record): TaskRecord { + return { + id: String(row.id), + runId: String(row.run_id), + title: String(row.title), + objective: String(row.objective), + acceptanceCriteria: parseJson(String(row.acceptance_criteria_json)), + verificationSteps: parseJson(String(row.verification_steps_json)), + allowedPaths: parseJson(String(row.allowed_paths_json)), + status: row.status as TaskStatus, + attemptCount: Number(row.attempt_count), + implementationSummary: row.implementation_summary ? String(row.implementation_summary) : null, + blockerSignature: row.blocker_signature ? String(row.blocker_signature) : null, + createdAt: String(row.created_at), + updatedAt: String(row.updated_at), + }; +} + +function mapAttemptRow(row: Record): TaskAttemptRecord { + return { + id: String(row.id), + runId: String(row.run_id), + taskId: String(row.task_id), + attemptNumber: Number(row.attempt_number), + status: row.status as AttemptStatus, + summary: String(row.summary), + blockerSignature: row.blocker_signature ? String(row.blocker_signature) : null, + resultJson: String(row.result_json), + createdAt: String(row.created_at), + }; +} + +function mapRunRow(row: Record): RunRecord { + return { + id: String(row.id), + goal: String(row.goal), + repoPath: String(row.repo_path), + status: row.status as RunStatus, + summary: row.summary ? String(row.summary) : null, + cycleCount: Number(row.cycle_count), + replanCount: Number(row.replan_count), + currentTaskId: row.current_task_id ? String(row.current_task_id) : null, + createdAt: String(row.created_at), + updatedAt: String(row.updated_at), + }; +} + +function mapAgentRow(row: Record): AgentStateRecord { + return { + runId: String(row.run_id), + role: row.role as AgentRole, + sessionId: row.session_id ? String(row.session_id) : null, + turns: Number(row.turns), + rotationCount: Number(row.rotation_count), + lastPromptPath: row.last_prompt_path ? String(row.last_prompt_path) : null, + lastResponsePath: row.last_response_path ? String(row.last_response_path) : null, + updatedAt: String(row.updated_at), + }; +} + +export class RunStore { + private readonly db: DatabaseSync; + + constructor(dbPath: string) { + ensureDir(path.dirname(dbPath)); + this.db = new DatabaseSync(dbPath); + this.db.exec("PRAGMA journal_mode = WAL;"); + this.db.exec(` + CREATE TABLE IF NOT EXISTS runs ( + id TEXT PRIMARY KEY, + goal TEXT NOT NULL, + repo_path TEXT NOT NULL, + status TEXT NOT NULL, + summary TEXT, + cycle_count INTEGER NOT NULL DEFAULT 0, + replan_count INTEGER NOT NULL DEFAULT 0, + current_task_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS agents ( + run_id TEXT NOT NULL, + role TEXT NOT NULL, + session_id TEXT, + turns INTEGER NOT NULL DEFAULT 0, + rotation_count INTEGER NOT NULL DEFAULT 0, + last_prompt_path TEXT, + last_response_path TEXT, + updated_at TEXT NOT NULL, + PRIMARY KEY (run_id, role) + ); + + CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + title TEXT NOT NULL, + objective TEXT NOT NULL, + acceptance_criteria_json TEXT NOT NULL, + verification_steps_json TEXT NOT NULL, + allowed_paths_json TEXT NOT NULL, + status TEXT NOT NULL, + attempt_count INTEGER NOT NULL DEFAULT 0, + implementation_summary TEXT, + blocker_signature TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS task_attempts ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + task_id TEXT NOT NULL, + attempt_number INTEGER NOT NULL, + status TEXT NOT NULL, + summary TEXT NOT NULL, + blocker_signature TEXT, + result_json TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id TEXT NOT NULL, + ts TEXT NOT NULL, + source TEXT NOT NULL, + kind TEXT NOT NULL, + message TEXT NOT NULL, + payload_json TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS artifacts ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + role TEXT, + kind TEXT NOT NULL, + path TEXT NOT NULL, + created_at TEXT NOT NULL, + metadata_json TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS checkpoints ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + status TEXT NOT NULL, + payload_json TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS approvals ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + source TEXT NOT NULL, + verdict TEXT NOT NULL, + rationale TEXT NOT NULL, + payload_json TEXT NOT NULL, + created_at TEXT NOT NULL + ); + `); + } + + createRun(goal: string, repoPath: string): RunRecord { + const timestamp = nowIso(); + const id = randomUUID(); + this.db + .prepare(` + INSERT INTO runs (id, goal, repo_path, status, summary, cycle_count, replan_count, current_task_id, created_at, updated_at) + VALUES (?, ?, ?, 'planning', NULL, 0, 0, NULL, ?, ?) + `) + .run(id, goal, repoPath, timestamp, timestamp); + + this.addEvent(id, "system", "run_created", "Run created.", { goal, repoPath }); + return this.getRun(id); + } + + getRun(runId: string): RunRecord { + const row = this.db.prepare("SELECT * FROM runs WHERE id = ?").get(runId) as Record | undefined; + if (!row) { + throw new Error(`Run not found: ${runId}`); + } + return mapRunRow(row); + } + + listRuns(): RunRecord[] { + const rows = this.db.prepare("SELECT * FROM runs ORDER BY created_at DESC").all() as Record[]; + return rows.map(mapRunRow); + } + + updateRun(runId: string, updates: Partial>): RunRecord { + const current = this.getRun(runId); + const next: RunRecord = { + ...current, + status: updates.status ?? current.status, + summary: updates.summary ?? current.summary, + currentTaskId: updates.currentTaskId === undefined ? current.currentTaskId : updates.currentTaskId, + updatedAt: nowIso(), + }; + this.db + .prepare(` + UPDATE runs + SET status = ?, summary = ?, current_task_id = ?, updated_at = ? + WHERE id = ? + `) + .run(next.status, next.summary, next.currentTaskId, next.updatedAt, runId); + return this.getRun(runId); + } + + incrementCycle(runId: string): void { + this.db.prepare("UPDATE runs SET cycle_count = cycle_count + 1, updated_at = ? WHERE id = ?").run(nowIso(), runId); + } + + incrementReplanCount(runId: string): void { + this.db.prepare("UPDATE runs SET replan_count = replan_count + 1, updated_at = ? WHERE id = ?").run(nowIso(), runId); + } + + getAgentState(runId: string, role: AgentRole): AgentStateRecord | null { + const row = this.db.prepare("SELECT * FROM agents WHERE run_id = ? AND role = ?").get(runId, role) as + | Record + | undefined; + return row ? mapAgentRow(row) : null; + } + + saveAgentState(record: AgentStateRecord): void { + this.db + .prepare(` + INSERT INTO agents (run_id, role, session_id, turns, rotation_count, last_prompt_path, last_response_path, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(run_id, role) DO UPDATE SET + session_id = excluded.session_id, + turns = excluded.turns, + rotation_count = excluded.rotation_count, + last_prompt_path = excluded.last_prompt_path, + last_response_path = excluded.last_response_path, + updated_at = excluded.updated_at + `) + .run( + record.runId, + record.role, + record.sessionId, + record.turns, + record.rotationCount, + record.lastPromptPath, + record.lastResponsePath, + record.updatedAt, + ); + } + + requeueInProgressTasks(runId: string): number { + const timestamp = nowIso(); + const result = this.db + .prepare(` + UPDATE tasks + SET status = 'pending', updated_at = ? + WHERE run_id = ? AND status = 'in_progress' + `) + .run(timestamp, runId); + const changes = Number(result.changes ?? 0); + if (changes > 0) { + this.addEvent(runId, "system", "requeue_in_progress", "Requeued in-progress tasks on resume.", { changes }); + } + return changes; + } + + replacePendingTasks(runId: string, tasks: TaskDraft[]): TaskRecord[] { + const now = nowIso(); + this.db + .prepare(` + UPDATE tasks + SET status = 'abandoned', updated_at = ? + WHERE run_id = ? AND status IN ('pending', 'in_progress') + `) + .run(now, runId); + + const insertStmt = this.db.prepare(` + INSERT INTO tasks ( + id, run_id, title, objective, acceptance_criteria_json, verification_steps_json, + allowed_paths_json, status, attempt_count, implementation_summary, blocker_signature, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, NULL, NULL, ?, ?) + `); + + const records: TaskRecord[] = []; + for (const task of tasks) { + const id = randomUUID(); + insertStmt.run( + id, + runId, + task.title, + task.objective, + toJson(task.acceptanceCriteria), + toJson(task.verificationSteps), + toJson(task.allowedPaths), + now, + now, + ); + records.push(this.getTask(id)); + } + + return records; + } + + listTasks(runId: string, statuses?: TaskStatus[]): TaskRecord[] { + if (!statuses?.length) { + const rows = this.db + .prepare("SELECT * FROM tasks WHERE run_id = ? ORDER BY created_at ASC") + .all(runId) as Record[]; + return rows.map(mapTaskRow); + } + + const placeholders = statuses.map(() => "?").join(", "); + const rows = this.db + .prepare(`SELECT * FROM tasks WHERE run_id = ? AND status IN (${placeholders}) ORDER BY created_at ASC`) + .all(runId, ...statuses) as Record[]; + return rows.map(mapTaskRow); + } + + getTask(taskId: string): TaskRecord { + const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId) as Record | undefined; + if (!row) { + throw new Error(`Task not found: ${taskId}`); + } + return mapTaskRow(row); + } + + claimNextPendingTask(runId: string): TaskRecord | null { + const row = this.db + .prepare(` + SELECT * FROM tasks + WHERE run_id = ? AND status = 'pending' + ORDER BY created_at ASC + LIMIT 1 + `) + .get(runId) as Record | undefined; + + if (!row) { + return null; + } + + const taskId = String(row.id); + this.db + .prepare(` + UPDATE tasks + SET status = 'in_progress', attempt_count = attempt_count + 1, updated_at = ? + WHERE id = ? + `) + .run(nowIso(), taskId); + return this.getTask(taskId); + } + + completeTask(taskId: string, summary: string): TaskRecord { + this.db + .prepare(` + UPDATE tasks + SET status = 'completed', implementation_summary = ?, blocker_signature = NULL, updated_at = ? + WHERE id = ? + `) + .run(summary, nowIso(), taskId); + return this.getTask(taskId); + } + + abandonTask(taskId: string, summary: string | null, blockerSignature: string | null): TaskRecord { + this.db + .prepare(` + UPDATE tasks + SET status = 'abandoned', implementation_summary = ?, blocker_signature = ?, updated_at = ? + WHERE id = ? + `) + .run(summary, blockerSignature, nowIso(), taskId); + return this.getTask(taskId); + } + + blockTask(taskId: string, summary: string | null, blockerSignature: string | null): TaskRecord { + this.db + .prepare(` + UPDATE tasks + SET status = 'blocked', implementation_summary = ?, blocker_signature = ?, updated_at = ? + WHERE id = ? + `) + .run(summary, blockerSignature, nowIso(), taskId); + return this.getTask(taskId); + } + + latestTaskAttempt(taskId: string): TaskAttemptRecord | null { + const row = this.db + .prepare("SELECT * FROM task_attempts WHERE task_id = ? ORDER BY created_at DESC LIMIT 1") + .get(taskId) as Record | undefined; + return row ? mapAttemptRow(row) : null; + } + + addTaskAttempt( + runId: string, + taskId: string, + attemptNumber: number, + status: AttemptStatus, + summary: string, + result: unknown, + blockerSignature: string | null, + ): TaskAttemptRecord { + const id = randomUUID(); + const createdAt = nowIso(); + this.db + .prepare(` + INSERT INTO task_attempts (id, run_id, task_id, attempt_number, status, summary, blocker_signature, result_json, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + .run(id, runId, taskId, attemptNumber, status, summary, blockerSignature, toJson(result), createdAt); + return this.latestTaskAttempt(taskId) as TaskAttemptRecord; + } + + addEvent(runId: string, source: string, kind: string, message: string, payload: unknown): void { + this.db + .prepare(` + INSERT INTO events (run_id, ts, source, kind, message, payload_json) + VALUES (?, ?, ?, ?, ?, ?) + `) + .run(runId, nowIso(), source, kind, message, toJson(payload)); + } + + listEvents(runId: string, limit = 50): EventRecord[] { + const rows = this.db + .prepare(` + SELECT * FROM events + WHERE run_id = ? + ORDER BY id DESC + LIMIT ? + `) + .all(runId, limit) as Record[]; + return rows + .map((row) => ({ + id: Number(row.id), + runId: String(row.run_id), + ts: String(row.ts), + source: String(row.source), + kind: String(row.kind), + message: String(row.message), + payload: JSON.parse(String(row.payload_json)), + })) + .reverse(); + } + + addArtifact(runId: string, role: AgentRole | null, kind: string, filePath: string, metadata: unknown): ArtifactRecord { + const id = randomUUID(); + const createdAt = nowIso(); + this.db + .prepare(` + INSERT INTO artifacts (id, run_id, role, kind, path, created_at, metadata_json) + VALUES (?, ?, ?, ?, ?, ?, ?) + `) + .run(id, runId, role, kind, filePath, createdAt, toJson(metadata)); + + return { + id, + runId, + role, + kind, + path: filePath, + createdAt, + metadata, + }; + } + + listArtifacts(runId: string, role?: AgentRole): ArtifactRecord[] { + const rows = role + ? (this.db + .prepare(` + SELECT * FROM artifacts + WHERE run_id = ? AND role = ? + ORDER BY created_at ASC + `) + .all(runId, role) as Record[]) + : (this.db + .prepare(` + SELECT * FROM artifacts + WHERE run_id = ? + ORDER BY created_at ASC + `) + .all(runId) as Record[]); + + return rows.map((row) => ({ + id: String(row.id), + runId: String(row.run_id), + role: row.role ? (String(row.role) as AgentRole) : null, + kind: String(row.kind), + path: String(row.path), + createdAt: String(row.created_at), + metadata: JSON.parse(String(row.metadata_json)), + })); + } + + addApproval( + runId: string, + source: ApprovalSource, + verdict: ApprovalVerdict, + rationale: string, + payload: unknown, + ): ApprovalRecord { + const id = randomUUID(); + const createdAt = nowIso(); + this.db + .prepare(` + INSERT INTO approvals (id, run_id, source, verdict, rationale, payload_json, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `) + .run(id, runId, source, verdict, rationale, toJson(payload), createdAt); + + return { + id, + runId, + source, + verdict, + rationale, + payload, + createdAt, + }; + } + + listApprovals(runId: string): ApprovalRecord[] { + const rows = this.db + .prepare("SELECT * FROM approvals WHERE run_id = ? ORDER BY created_at ASC") + .all(runId) as Record[]; + return rows.map((row) => ({ + id: String(row.id), + runId: String(row.run_id), + source: row.source as ApprovalSource, + verdict: row.verdict as ApprovalVerdict, + rationale: String(row.rationale), + payload: JSON.parse(String(row.payload_json)), + createdAt: String(row.created_at), + })); + } + + latestApproval(runId: string, source: ApprovalSource): ApprovalRecord | null { + const row = this.db + .prepare(` + SELECT * FROM approvals + WHERE run_id = ? AND source = ? + ORDER BY created_at DESC + LIMIT 1 + `) + .get(runId, source) as Record | undefined; + if (!row) { + return null; + } + + return { + id: String(row.id), + runId: String(row.run_id), + source: row.source as ApprovalSource, + verdict: row.verdict as ApprovalVerdict, + rationale: String(row.rationale), + payload: JSON.parse(String(row.payload_json)), + createdAt: String(row.created_at), + }; + } + + addCheckpoint(runId: string, status: RunStatus, payload: unknown): CheckpointRecord { + const id = randomUUID(); + const createdAt = nowIso(); + this.db + .prepare(` + INSERT INTO checkpoints (id, run_id, status, payload_json, created_at) + VALUES (?, ?, ?, ?, ?) + `) + .run(id, runId, status, toJson(payload), createdAt); + + return { + id, + runId, + status, + payload, + createdAt, + }; + } + + buildSnapshot(runId: string): RunSnapshot { + const run = this.getRun(runId); + const agents = (["strategy", "implementation", "checker"] as const).reduce((acc, role) => { + const agent = this.getAgentState(runId, role); + if (agent) { + acc[role] = agent; + } + return acc; + }, {}); + + return { + run, + agents, + pendingTasks: this.listTasks(runId, ["pending"]), + inProgressTasks: this.listTasks(runId, ["in_progress"]), + completedTasks: this.listTasks(runId, ["completed"]), + blockedTasks: this.listTasks(runId, ["blocked", "abandoned"]), + recentAttempts: this.listRecentAttempts(runId), + recentEvents: this.listEvents(runId), + approvals: this.listApprovals(runId), + artifacts: this.listArtifacts(runId), + }; + } + + listRecentAttempts(runId: string, limit = 12): TaskAttemptRecord[] { + const rows = this.db + .prepare(` + SELECT * FROM task_attempts + WHERE run_id = ? + ORDER BY created_at DESC + LIMIT ? + `) + .all(runId, limit) as Record[]; + return rows.map(mapAttemptRow).reverse(); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0622862 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,230 @@ +export const agentRoles = ["strategy", "implementation", "checker"] as const; + +export type AgentRole = (typeof agentRoles)[number]; +export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"; +export type RunStatus = + | "planning" + | "implementing" + | "self_check" + | "independent_check" + | "blocked" + | "done"; +export type TaskStatus = "pending" | "in_progress" | "completed" | "blocked" | "abandoned"; +export type AttemptStatus = "completed" | "needs_replan" | "blocked"; +export type ApprovalSource = "strategy_self_check" | "checker"; +export type ApprovalVerdict = "approved" | "rejected"; + +export interface RoleConfig { + model?: string; + sandbox: SandboxMode; + search: boolean; + skipGitRepoCheck: boolean; + extraArgs: string[]; +} + +export interface ResolvedConfig { + repoPath: string; + stateDir: string; + databasePath: string; + runsDir: string; + codexCommand: string; + maxTaskAttempts: number; + maxCyclesPerInvocation: number; + maxReplans: number; + maxTasks: number; + sessionRefreshTurns: number; + roles: Record; +} + +export interface RunRecord { + id: string; + goal: string; + repoPath: string; + status: RunStatus; + summary: string | null; + cycleCount: number; + replanCount: number; + currentTaskId: string | null; + createdAt: string; + updatedAt: string; +} + +export interface AgentStateRecord { + runId: string; + role: AgentRole; + sessionId: string | null; + turns: number; + rotationCount: number; + lastPromptPath: string | null; + lastResponsePath: string | null; + updatedAt: string; +} + +export interface TaskDraft { + title: string; + objective: string; + acceptanceCriteria: string[]; + verificationSteps: string[]; + allowedPaths: string[]; +} + +export interface TaskRecord extends TaskDraft { + id: string; + runId: string; + status: TaskStatus; + attemptCount: number; + implementationSummary: string | null; + blockerSignature: string | null; + createdAt: string; + updatedAt: string; +} + +export interface TaskAttemptRecord { + id: string; + runId: string; + taskId: string; + attemptNumber: number; + status: AttemptStatus; + summary: string; + blockerSignature: string | null; + resultJson: string; + createdAt: string; +} + +export interface EventRecord { + id: number; + runId: string; + ts: string; + source: string; + kind: string; + message: string; + payload: unknown; +} + +export interface ArtifactRecord { + id: string; + runId: string; + role: AgentRole | null; + kind: string; + path: string; + createdAt: string; + metadata: unknown; +} + +export interface ApprovalRecord { + id: string; + runId: string; + source: ApprovalSource; + verdict: ApprovalVerdict; + rationale: string; + payload: unknown; + createdAt: string; +} + +export interface CheckpointRecord { + id: string; + runId: string; + status: RunStatus; + payload: unknown; + createdAt: string; +} + +export interface VerificationResult { + command: string; + outcome: "passed" | "failed" | "not_run"; + details: string; +} + +export interface StrategyPlanOutput { + decision: "continue" | "done" | "blocked"; + summary: string; + rationale: string; + goalProgress: string; + risks: string[]; + tasks: TaskDraft[]; + blockedReason?: string; +} + +export interface StrategySelfCheckOutput { + readyForIndependentCheck: boolean; + summary: string; + rationale: string; + evidence: string[]; + remainingGaps: string[]; +} + +export interface ImplementationResultOutput { + taskId: string; + status: AttemptStatus; + summary: string; + changes: string[]; + verification: VerificationResult[]; + followUps: string[]; + touchedFiles: string[]; + blockers: string[]; +} + +export interface CheckVerdictOutput { + verdict: "approved" | "rejected"; + summary: string; + rationale: string; + evidence: string[]; + remainingTasks: TaskDraft[]; +} + +export interface RunSnapshot { + run: RunRecord; + agents: Partial>; + pendingTasks: TaskRecord[]; + inProgressTasks: TaskRecord[]; + completedTasks: TaskRecord[]; + blockedTasks: TaskRecord[]; + recentAttempts: TaskAttemptRecord[]; + recentEvents: EventRecord[]; + approvals: ApprovalRecord[]; + artifacts: ArtifactRecord[]; +} + +export interface InvocationArtifacts { + promptPath: string; + schemaPath: string; + rawEventsPath: string; + stderrPath: string; + lastMessagePath: string; + responsePath: string; +} + +export interface AgentInvocation { + runId: string; + role: AgentRole; + sessionId: string | null; + prompt: string; + schemaName: string; + schema: Record; + cwd: string; + roleConfig: RoleConfig; + artifacts: InvocationArtifacts; + maxValidationRetries?: number; +} + +export interface AgentInvocationResult { + sessionId: string | null; + output: T; + rawMessage: string; + rawEvents: string[]; + stderr: string; + artifacts: InvocationArtifacts; +} + +export interface RawAgentRunner { + invoke(request: AgentInvocation): Promise>; +} + +export interface RunArtifactsIndex { + promptPath: string; + responsePath: string; + schemaPath: string; + rawEventsPath: string; + stderrPath: string; + lastMessagePath: string; +} diff --git a/test/orchestrator.test.ts b/test/orchestrator.test.ts new file mode 100644 index 0000000..48c7663 --- /dev/null +++ b/test/orchestrator.test.ts @@ -0,0 +1,138 @@ +import { mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { ArtifactManager } from "../src/artifacts.js"; +import { loadConfig } from "../src/config.js"; +import { AgentROrchestrator } from "../src/orchestrator.js"; +import { RunStore } from "../src/store.js"; +import type { AgentInvocation, AgentInvocationResult, RawAgentRunner } from "../src/types.js"; + +class ScriptedRunner implements RawAgentRunner { + public readonly prompts: string[] = []; + + constructor( + private readonly outputs: string[], + ) {} + + async invoke(request: AgentInvocation): Promise> { + const next = this.outputs.shift(); + if (!next) { + throw new Error("No scripted output remaining."); + } + + this.prompts.push(request.prompt); + readFileSync(request.artifacts.schemaPath, "utf8"); + const parsed = JSON.parse(next) as Record; + if (typeof parsed.taskId === "string" && !parsed.taskId) { + const match = request.prompt.match(/exact task id `([^`]+)`/); + if (match) { + parsed.taskId = match[1]; + } + } + return { + sessionId: request.sessionId ?? `session-${this.prompts.length}`, + output: parsed as T, + rawMessage: JSON.stringify(parsed), + rawEvents: ['{"type":"turn.completed"}'], + stderr: "", + artifacts: request.artifacts, + }; + } +} + +describe("AgentROrchestrator", () => { + test("runs through plan, implementation, self-check, and independent check", async () => { + const repoPath = mkdtempSync(path.join(tmpdir(), "agent-r-orchestrator-")); + const config = loadConfig(repoPath); + const store = new RunStore(config.databasePath); + const artifacts = new ArtifactManager(config.runsDir); + const runner = new ScriptedRunner([ + JSON.stringify({ + decision: "continue", + summary: "Implement the CLI skeleton.", + rationale: "The project is empty and needs a first vertical slice.", + goalProgress: "No implementation exists yet.", + risks: [], + tasks: [ + { + title: "Create CLI entrypoint", + objective: "Build the initial command surface.", + acceptanceCriteria: ["A run command exists."], + verificationSteps: ["Run the help command."], + allowedPaths: ["src", "package.json"], + }, + ], + }), + JSON.stringify({ + taskId: "", + status: "completed", + summary: "Added the CLI skeleton.", + changes: ["Created an entrypoint."], + verification: [{ command: "agent-r --help", outcome: "passed", details: "Help output rendered." }], + followUps: [], + touchedFiles: ["src/index.ts", "package.json"], + blockers: [], + }), + JSON.stringify({ + decision: "done", + summary: "The requested slice is complete.", + rationale: "The only planned task is complete and verified.", + goalProgress: "The vertical slice exists.", + risks: [], + tasks: [], + }), + JSON.stringify({ + readyForIndependentCheck: true, + summary: "The run is ready for independent review.", + rationale: "Implementation and verification are present.", + evidence: ["CLI entrypoint exists."], + remainingGaps: [], + }), + JSON.stringify({ + verdict: "approved", + summary: "The goal is satisfied.", + rationale: "The requested slice exists and is verified.", + evidence: ["CLI entrypoint present."], + remainingTasks: [], + }), + ]); + + const orchestrator = new AgentROrchestrator(config, store, artifacts, runner); + const run = orchestrator.createRun("Create a CLI skeleton"); + + const firstStatus = await orchestrator.runUntilStable(run.id, 10); + const snapshot = store.buildSnapshot(run.id); + + expect(firstStatus.status).toBe("done"); + expect(snapshot.completedTasks).toHaveLength(1); + expect(snapshot.approvals).toHaveLength(2); + expect(snapshot.pendingTasks).toHaveLength(0); + expect(runner.prompts.at(1)).toContain("You must echo the exact task id"); + }); + + test("strategy prompt explicitly permits read-only repository inspection", async () => { + const repoPath = mkdtempSync(path.join(tmpdir(), "agent-r-strategy-")); + const config = loadConfig(repoPath); + const store = new RunStore(config.databasePath); + const artifacts = new ArtifactManager(config.runsDir); + const runner = new ScriptedRunner([ + JSON.stringify({ + decision: "blocked", + summary: "Cannot continue.", + rationale: "Test stop.", + goalProgress: "None.", + risks: [], + tasks: [], + blockedReason: "Stop immediately.", + }), + ]); + + const orchestrator = new AgentROrchestrator(config, store, artifacts, runner); + const run = orchestrator.createRun("Inspect repo"); + await orchestrator.runUntilStable(run.id, 1); + + expect(runner.prompts[0]).toContain("You may inspect the repository directly"); + expect(runner.prompts[0]).toContain("Do not edit files"); + }); +}); diff --git a/test/store.test.ts b/test/store.test.ts new file mode 100644 index 0000000..8ab2683 --- /dev/null +++ b/test/store.test.ts @@ -0,0 +1,40 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { RunStore } from "../src/store.js"; + +describe("RunStore", () => { + test("creates runs, tasks, and snapshots", () => { + const tempDir = mkdtempSync(path.join(tmpdir(), "agent-r-store-")); + const store = new RunStore(path.join(tempDir, "state.sqlite")); + const run = store.createRun("Build something", tempDir); + + store.replacePendingTasks(run.id, [ + { + title: "Task 1", + objective: "Do work", + acceptanceCriteria: ["It works"], + verificationSteps: ["Run tests"], + allowedPaths: ["src"], + }, + { + title: "Task 2", + objective: "Do more work", + acceptanceCriteria: ["It still works"], + verificationSteps: ["Run tests again"], + allowedPaths: ["tests"], + }, + ]); + + const claimed = store.claimNextPendingTask(run.id); + expect(claimed?.status).toBe("in_progress"); + store.completeTask(claimed!.id, "Finished"); + store.addEvent(run.id, "test", "custom", "Recorded a test event.", { ok: true }); + + const snapshot = store.buildSnapshot(run.id); + expect(snapshot.pendingTasks).toHaveLength(1); + expect(snapshot.completedTasks).toHaveLength(1); + expect(snapshot.recentEvents.at(-1)?.kind).toBe("custom"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..da8a441 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2023"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +}