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"]
+}