diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c69d48c52..7b38006e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,9 @@ concurrency: cancel-in-progress: ${{ github.event_name != 'push' }} jobs: - # Prettier, eslint, and typecheck share one runner: one checkout and one - # install instead of three of each. Prettier and eslint still run un-cached - # in CI: restored result caches can mark files clean against a stale tool - # version or config, masking real failures. The --cache flags in the - # package.json lint script remain for local speed. + # oxfmt, oxlint, and typecheck share one runner: one checkout and one + # install instead of three of each. Dummy job names prettier and eslint + # stay for protect-main. static-analysis: runs-on: ubuntu-latest steps: @@ -39,11 +37,11 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Prettier - run: bunx prettier --check . + - name: oxfmt + run: bunx oxfmt --check . - - name: ESLint - run: bunx eslint . + - name: oxlint + run: bunx oxlint . - name: Typecheck run: bun run typecheck diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..2335e6888 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,28 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "trailingComma": "all", + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "printWidth": 80, + "sortPackageJson": false, + "sortImports": false, + "ignorePatterns": [ + "dist/**", + "vendor/**", + ".worktrees/**", + "scratch/**", + "node_modules/**", + "CHANGELOG.md", + "tmp/**", + ".claude/**", + ".tmp/**", + "plugins/corbits-skills/skills/opsh/SKILL.md", + "plugins/corbits-skills/skills/refactor/SKILL.md", + "plugins/corbits-skills/skills/scribe/SKILL.md", + "plugins/corbits-skills/skills/ast-grep/SKILL.md", + "plugins/corbits-skills/skills/review/SKILL.md", + "plugins/corbits-skills/skills/create-issue/SKILL.md" + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..eb36056c9 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,106 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + // Type-aware rules (no-floating-promises, no-misused-promises, + // no-unsafe-type-assertion) stay on tsc until oxlint can honor them. + "plugins": ["eslint", "typescript"], + "jsPlugins": ["./scripts/oxlint-plugin-corbits.js"], + "categories": { + "correctness": "error" + }, + "ignorePatterns": [ + "dist/**", + "vendor/**", + ".worktrees/**", + "**/.worktrees/**", + ".scratch/**", + "**/.scratch/**", + "scratch/**", + "**/scratch/**", + "tmp/**", + "**/tmp/**", + ".claude/**", + "**/.claude/**", + ".tmp/**", + "**/.tmp/**", + "node_modules/**", + "**/node_modules/**" + ], + "options": { + "reportUnusedDisableDirectives": "error" + }, + "rules": { + "no-console": "error", + "no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "no-unused-expressions": ["error", { "allowTaggedTemplates": true }], + "typescript/adjacent-overload-signatures": "error", + "typescript/array-type": "error", + "typescript/ban-ts-comment": "error", + "typescript/ban-tslint-comment": "error", + "typescript/class-literal-property-style": "error", + "typescript/consistent-generic-constructors": "error", + "typescript/consistent-indexed-object-style": "error", + "typescript/consistent-type-assertions": "error", + "typescript/consistent-type-definitions": "off", + "typescript/no-confusing-non-null-assertion": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-dynamic-delete": "error", + "typescript/no-empty-interface": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-extraneous-class": "error", + "typescript/no-inferrable-types": "error", + "typescript/no-invalid-void-type": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-nullish-coalescing": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-non-null-assertion": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-for-of": "error", + "typescript/prefer-function-type": "error", + "typescript/prefer-literal-enum-member": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/unified-signatures": "error", + "no-empty-function": "error" + }, + "overrides": [ + { + "files": ["src/util/control-char-strip.ts"], + "rules": { + "no-control-regex": "off" + } + }, + { + "files": ["scripts/**"], + "rules": { + "no-console": "off" + } + }, + { + "files": ["src/tui/smoke.ts", "src/tui/demo.ts"], + "rules": { + "no-console": "off" + } + }, + { + "files": ["**/*.test.ts"], + "rules": { + "corbits/no-bare-mock-module": "error" + } + } + ] +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 65b2f9712..000000000 --- a/.prettierignore +++ /dev/null @@ -1,16 +0,0 @@ -dist/ -vendor/ -.worktrees/ -scratch/ -node_modules/ -CHANGELOG.md -plugins/corbits-skills/skills/opsh/SKILL.md -plugins/corbits-skills/skills/refactor/SKILL.md -plugins/corbits-skills/skills/scribe/SKILL.md -plugins/corbits-skills/skills/ast-grep/SKILL.md -plugins/corbits-skills/skills/review/SKILL.md -plugins/corbits-skills/skills/create-issue/SKILL.md - -tmp/ -.claude/ -.tmp/ diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 91f81d7a5..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "semi": true, - "trailingComma": "all", - "printWidth": 100 -} diff --git a/AGENTS.md b/AGENTS.md index 7a0de1f4f..7cfc4a432 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,8 +34,8 @@ When refactoring replaces an old path, delete the old one. No back-compat shims, - Bug fixes start with a failing test that reproduces the bug. Do not start by patching. - `tests/unit/` shared unit tests and helpers · co-located `src/**/*.test.ts` for module logic · `tests/fixtures/` fixture repos · `tests/integration/` reactor/permission harness. Planned: `tests/e2e/` (fixture-repo runs). - A test must not depend on another file having run, or on the default file order. It must pass under `bun test ./src ./tests ./evals ./scripts --randomize`. If a test mutates module-level state or calls `mock.module`, it must restore that state itself (`afterEach`/`afterAll`), not rely on the process happening to reset it. When capturing a module's real exports to restore later, shallow-copy them (`{ ...moduleNamespace }`) at capture time, whether the namespace came from `await import(path)` or a static `import * as ns from "path"` — Bun mutates the live namespace object in place when the module is mocked, so holding a bare reference to it (either form) silently turns into the mocked exports. -- Never call `mock.module` directly. Bun runs every test file in one process, so a `mock.module` call without its own teardown stays installed for the rest of the run and silently replaces the real module for other files — producing failures in files the change never touched, with no obvious link to the cause and no signal from `tsc` or a per-file run (CL-6967). Use `withMockedModule`/`withMockedModuleDuring` from `tests/helpers/mock-module.ts`, which capture the real module and register their own restore. An eslint rule (`no-restricted-syntax` in `eslint.config.js`) rejects bare `mock.module` calls in `*.test.ts` files. -- A test earns its place only if a real behavior change can fail it. Document copy, brand colors, marketing assets, and splash text are not behavior: assertions that pin an asset's literal wording, an exact palette hex/ANSI value, or rendered copy fail on copy/design edits and catch no regressions — assert the contract instead (parsing, formatting, ranges, aliases, invariants). Tests are code too: pinning a source file's own text is the same trap. This bar is a review and authorship rule, not an eslint shape match. +- Never call `mock.module` directly. Bun runs every test file in one process, so a `mock.module` call without its own teardown stays installed for the rest of the run and silently replaces the real module for other files — producing failures in files the change never touched, with no obvious link to the cause and no signal from `tsc` or a per-file run (CL-6967). Use `withMockedModule`/`withMockedModuleDuring` from `tests/helpers/mock-module.ts`, which capture the real module and register their own restore. The oxlint plugin (`corbits/no-bare-mock-module` in `.oxlintrc.json` / `scripts/oxlint-plugin-corbits.js`) rejects bare `mock.module` calls in `*.test.ts` files. +- A test earns its place only if a real behavior change can fail it. Document copy, brand colors, marketing assets, and splash text are not behavior: assertions that pin an asset's literal wording, an exact palette hex/ANSI value, or rendered copy fail on copy/design edits and catch no regressions — assert the contract instead (parsing, formatting, ranges, aliases, invariants). Tests are code too: pinning a source file's own text is the same trap. This bar is a review and authorship rule, not a linter shape match. ## Build & Validation diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c3a3d9f..a82ea177b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,10 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename ### Changed +- oxfmt formats the tree and oxlint is the lint gate. Prettier and ESLint are + gone. Dummy CI job names `prettier` and `eslint` stay for protect-main. + Empty functions and non-null assertions are errors. + - Skywalker may spawn one successor with a changed brief after a failed or incomplete-report fleet worker. A parent-initiated interrupt (`stop_reason: interrupted`) is a resumable pause — `resume_agent` or diff --git a/bun.lock b/bun.lock index 3434dbde7..c961244f3 100644 --- a/bun.lock +++ b/bun.lock @@ -27,13 +27,11 @@ "isomorphic-git": "catalog:", }, "devDependencies": { - "@eslint/js": "^9.39.0", "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", - "eslint": "^9.39.0", - "prettier": "^3.6.2", + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", "typescript": "5.9.3", - "typescript-eslint": "^8.46.4", "typescript-language-server": "^4.3.4", "ws": "^8.21.0", }, @@ -209,38 +207,10 @@ "@corbits/provider-opencode-go": ["@corbits/provider-opencode-go@workspace:packages/opencode-go"], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.7", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.2", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw=="], - - "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], - "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - - "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], - - "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@intx/agent": ["@intx/agent@workspace:vendor/intx-agent"], "@intx/authz": ["@intx/authz@workspace:vendor/intx-authz"], @@ -323,56 +293,100 @@ "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ulfw8EHN1MBq/MFFDXw2/M1VAFu5mRUcnuZ8Hqbv9viAnFzO9t1jKSAsDqKYYDGMlytF/uj6Z5z5n/tHupnKhw=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MfONZx/O2o9M5v2jDFol556G9+A+P9xCuJ4DZ+qhE+RnaCdoscy6Eu5nq1dbuNxhwdJyZ6kLI7fnG9mwEeOeGg=="], - "@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CYnIx5LvFVJnyJcCqwH2jxMKjFjqo5678MPjdmNFoSGMhlOvZ/xRZqvhDcolKrXc8fezW3AKh+C4wyoFuWOSSg=="], - "@types/semver": ["@types/semver@7.8.0", "", {}, "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.67.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7/iF1orvIS9mxhKUqnmtMgm+OrSQ5acPwuvdQrm6ECgqbwPmC+Pw9cdke3sNfVN6pT2hbJ58+jP8BCThl5HXOg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.70.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/type-utils": "8.70.0", "@typescript-eslint/utils": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.70.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-yy+OGys07IZOpOmYPZoObKyUQLkfxeQqeCypk+1jaZd8HGo77hzvU1Jg8X3+W75o+9lszOjBfg0nkGtlwYywXw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.70.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wPIeeigXgJpwNw3wydYRt3U9iN9Y/ejpOZuYL9IA7igxWs7LIQMOkhKxTumRvy6dIv0iXKk3RTw3Vmjg0i+2sg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.70.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.70.0", "@typescript-eslint/types": "^8.70.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0+XNxcdbkTfxdcD4qW6Ci9n+mBNJ8xTBumnxKvKBmRFOdx0Wf8/KiHjCJayooXmYkqRpRVd98Q5egvzx5BLSgQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0" } }, "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-I75LKPJyNOYUzkqAiAMIE31+Ye7xtQXZdoty1IXn4B+bw5Zpmez5wfG19ejGpNnS/BzQ7LFS+7jxuTPb+vHiZw=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.70.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw=="], + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.67.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-c2M5iRpe1QMZSRE/UvZoPdXBWb5Ic/ycvOyNiKCqPwQ/OyOKIMiJs02ynlNnjb7ZZJnRXYLmGcohoINOcwDK3w=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/utils": "8.70.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw=="], + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-dQzzYlV24Udhfm5ECuSdgqRvFJU/CGHzcYYEO3dLM6W6+CHiBFrq9OjIllkdCcPhsoSQ8o223Dja84MOSzed9A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.70.0", "", {}, "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ=="], + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-rFNq1CgX4qMJANOq42LkAs90JE80GpiaEohAV2qn/gT2hGjQTW1zBO5zQBxArI4926pM1OSzo3CN0tBszGBIaA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.70.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.70.0", "@typescript-eslint/tsconfig-utils": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA=="], + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.67.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Sky6rEdz2o5IGq01lPhS12yEvDdChVEcaYrcLHkveh4Fx0qPjljE/Iul6SX/bRMl6lNc8J7J/mDQdzgBdA++Pg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.70.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA=="], + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vPXmlNORV8AZq2Ocxh07pxwMjfENUWCV/eZArnao0qC3NO/hDeTVkQvee7SJJUbIiF5PZbBa4kYmaXnu7Rk58w=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ=="], + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-x/WAtFqYtVr3vZ9ni8nr4kn9whSitg8fOljq/pZzBpxopRdY1BMLZCZkrbIbaBcYkm46qGbqVea2FCWmtQ2P9w=="], - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.67.0", "", { "os": "none", "cpu": "arm64" }, "sha512-eRw9Neh4/aA6i+q/R3WU1gGQINhVM0J4fXIm6t27caOamkr/37uAkp1IdBx4zlJH97hmXR63z/q9n5c5dN7MzA=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.67.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YIMvb+sGNYN2uc6+QK2HLPeEKM2vl7QZ5onQzpAJRb6pnf0DwUFP5R8tdS9R0l8hdUil2gu4Uxd0Yxrop0iT4w=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.67.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-LzmU9MyACPzwNDIK0ItMedHPz735Ug7ELWguxo4/kuy6zWuDoeglOAEFCY8jLg0PzRpFO3hDyLFe2Gu2eFDeGA=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbQOIDNLUEeVZcAJcSL5VURn7kfjvilPviody4Yl5n8lQCDtUm+C9oHTTwZS/m4d/Z6Vv3jNEiAofH932NPPCg=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.82.0", "", { "os": "android", "cpu": "arm" }, "sha512-a3LB+C5Dsj5b/qtmG/mv5WrzuiXEpg1KF5nXWcEvaoN5TYAqkIvxPOwTPp3Jy/FoGpRo8zsTFhMElMXfeoOEzA=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.82.0", "", { "os": "android", "cpu": "arm64" }, "sha512-OBlhRgNqFblGpGenno/aqOfJLOkQ2B8Ig3iDAalfn0H8hJGZKXPeexCRTDm6uwv6YUjSA9Xnwt1y/Bgj5ZH8uw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.82.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dsopxqtY5ZdyT9uLHyGt1SyiLop6hi7hWI3PKpePodkRQOkLaCm+OE4fR9CAz9qdfjiFO8531tX/QDyP/psjFg=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.82.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-94Lu0SgTClKColU66g1VDuigV3HkcbkJBnTtZjGYfE8UPugaWDgKrm2icjC6HJVUYler2OXaHP/X0TBy8+CowQ=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.82.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hne/V06ewhh1i0w8+l7GDNROAGCGPmyFuOwiP7YTRu0JycyStJ4785dmF8xU5p0uUwt2emvIF9vc7Xjis+cJ0g=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.82.0", "", { "os": "linux", "cpu": "arm" }, "sha512-aWY2xtbZf1LneW9Qsv/n2Sp8gOu74JrlQzEtj4coHX2SHFrCfhmAumaU+sI/A5nr+yoTRTSmI/pL2s6ADlNSkw=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.82.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Fe+TtXCXMh/5f7kWlZ2VAwsMumZWtraFlKVk1NJlL52/beGwfDE7ov+/8gVirHzWokzGu7X65hSPq0ucPDskWQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.82.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6azCZ6OJudlvipNttXCCQcyeFfcJ/NvUZdSN1z8elo73kCHtyQC7WTiUcSjWYvJ1jaq9KDUyMAoAS/vNzhBomA=="], - "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.82.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PLEaSD8IAIIlwW4dwOd9YaxuxeOpwiXL4J24rcnE4iNtyM5j9Q9/3+gti08oXpx0u2ygNjRDx9xjWWpQonuJEw=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.82.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D94em/BwknNTn4vqxjHh5wb2oL566eFhArabqKIr0cNZMHOJuiraFp1A8tXpH05bbE5tqwEfLXTI0MWEGtn3Dw=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.82.0", "", { "os": "linux", "cpu": "none" }, "sha512-MOprxBaoYU2D4VgxXCl3ghydThWtx7Um1lL51kGYNeQ5Al7WzsH7/tqGdNtbLrIWnjq3bsm13+nz/gRIxjrOXw=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.82.0", "", { "os": "linux", "cpu": "none" }, "sha512-5h55QsfJ/luDXZzC20k6SNOY1Az+dCP9WvntKtcUWh2JhckAdwApY2ZusaBTwLENnReXU+A2fJtSrYvZJNKNPg=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.82.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-IE8NJNLlHr0CaXyGJPGVn0eTkUyoj1I2UfA8x7I4PSOYKsQ/6btVC7Pywrj5onk0cMH25r6Z38SoN3AvE5Zuog=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.82.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XUUUxaBo9XKl+J1B9EmP1cTGQPddzeURvoGkfwh/94PGnbW+hBprDljneoI2M1jzC1bzrIV3ihc7iM9UXl8+tg=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.82.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SWLSFulX9TDuH6yvbPYp4+VNn6jkkIvvI+KiujDM5rWBRHEfkesCC/pCneIIUr6ovkxZ5fRtpi2v5Cz5FrMJZg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.82.0", "", { "os": "none", "cpu": "arm64" }, "sha512-BQy35f6ZUdNr9a6c7B7orxQTcLjByGT2z3WAgmRovpRwmPYAaJ+NTplmMzhdjdJ4qSchfMNZy/Ukg+qRg6zseQ=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.82.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V4QhSTg5gctZue8RJjsGi7NpQPThr/p1/HfmiMC5kfe1KFEup9SQRVub4A6kijQjdHfxj7bLL1KO3QO7/5bwMQ=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.82.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-TUSCLaKB2yktpFAJ/r3HAUYsaV/3DT7JS4iNKyoh3a9YNwD0UG7Ezh4D8m23654vQcU6P/RQrCAjRPKe4peP/A=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.82.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VTVoRIWJTb+wvUX8EYoPArfFH02whuR10goFXE/LHRRX33ajRrFgqbcONXZMiF4C5rnattfkm87HqYn8jb8hmQ=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="], + + "@types/semver": ["@types/semver@7.8.0", "", {}, "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "arkregex": ["arkregex@0.0.8", "", { "dependencies": { "@ark/util": "0.56.2" } }, "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ=="], "arktype": ["arktype@2.2.3", "", { "dependencies": { "@ark/schema": "0.56.2", "@ark/util": "0.56.2", "arkregex": "0.0.8" } }, "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg=="], @@ -381,13 +395,13 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - "brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -405,20 +419,10 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "clean-git-ref": ["clean-git-ref@2.0.1", "", {}, "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -437,8 +441,6 @@ "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -465,24 +467,6 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -499,26 +483,12 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-text-encoding": ["fast-text-encoding@1.0.6", "", {}, "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="], "fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], @@ -537,14 +507,8 @@ "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -573,10 +537,6 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ip-address": ["ip-address@10.7.0", "", {}, "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA=="], @@ -585,10 +545,6 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], @@ -603,30 +559,16 @@ "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], - "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], "just-debounce-it": ["just-debounce-it@1.1.0", "", {}, "sha512-87Nnc0qZKgBZuhFZjYVjSraic0x7zwjhaTMrCKlj0QYKH6lh0KbFzVnfu6LHan03NO7J8ygjeBeD0epejn5Zcg=="], "just-once": ["just-once@1.1.0", "", {}, "sha512-+rZVpl+6VyTilK7vB/svlMPil4pxqIJZkbnN7DKZTOzyXfun6ZiFeq2Pk4EtCEHZ0VU4EkdFzG8ZK5F3PErcDw=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], @@ -645,7 +587,7 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -667,8 +609,6 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], "npm-install-checks": ["npm-install-checks@7.1.2", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ=="], @@ -689,30 +629,22 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "oxfmt": ["oxfmt@0.67.0", "", { "dependencies": { "tinypool": "2.1.2" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.67.0", "@oxfmt/binding-android-arm64": "0.67.0", "@oxfmt/binding-darwin-arm64": "0.67.0", "@oxfmt/binding-darwin-x64": "0.67.0", "@oxfmt/binding-freebsd-x64": "0.67.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.67.0", "@oxfmt/binding-linux-arm-musleabihf": "0.67.0", "@oxfmt/binding-linux-arm64-gnu": "0.67.0", "@oxfmt/binding-linux-arm64-musl": "0.67.0", "@oxfmt/binding-linux-ppc64-gnu": "0.67.0", "@oxfmt/binding-linux-riscv64-gnu": "0.67.0", "@oxfmt/binding-linux-riscv64-musl": "0.67.0", "@oxfmt/binding-linux-s390x-gnu": "0.67.0", "@oxfmt/binding-linux-x64-gnu": "0.67.0", "@oxfmt/binding-linux-x64-musl": "0.67.0", "@oxfmt/binding-openharmony-arm64": "0.67.0", "@oxfmt/binding-win32-arm64-msvc": "0.67.0", "@oxfmt/binding-win32-ia32-msvc": "0.67.0", "@oxfmt/binding-win32-x64-msvc": "0.67.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-vV7sSiPsaO0mSxdoUdayipVDFPzW/UQ+hrezEHa20+Tx1dnMdZLSRHMT0PdS67FFbhd74M1n08asW21aLGeCrA=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "oxlint": ["oxlint@1.82.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.82.0", "@oxlint/binding-android-arm64": "1.82.0", "@oxlint/binding-darwin-arm64": "1.82.0", "@oxlint/binding-darwin-x64": "1.82.0", "@oxlint/binding-freebsd-x64": "1.82.0", "@oxlint/binding-linux-arm-gnueabihf": "1.82.0", "@oxlint/binding-linux-arm-musleabihf": "1.82.0", "@oxlint/binding-linux-arm64-gnu": "1.82.0", "@oxlint/binding-linux-arm64-musl": "1.82.0", "@oxlint/binding-linux-ppc64-gnu": "1.82.0", "@oxlint/binding-linux-riscv64-gnu": "1.82.0", "@oxlint/binding-linux-riscv64-musl": "1.82.0", "@oxlint/binding-linux-s390x-gnu": "1.82.0", "@oxlint/binding-linux-x64-gnu": "1.82.0", "@oxlint/binding-linux-x64-musl": "1.82.0", "@oxlint/binding-openharmony-arm64": "1.82.0", "@oxlint/binding-win32-arm64-msvc": "1.82.0", "@oxlint/binding-win32-ia32-msvc": "1.82.0", "@oxlint/binding-win32-x64-msvc": "1.82.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ=="], "p-map": ["p-map@7.0.7", "", {}, "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], @@ -721,18 +653,12 @@ "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], - "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], @@ -743,8 +669,6 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -795,38 +719,26 @@ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tinypool": ["tinypool@2.1.2", "", {}, "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww=="], "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.70.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.70.0", "@typescript-eslint/parser": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/utils": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw=="], - "typescript-language-server": ["typescript-language-server@4.4.1", "", { "bin": { "typescript-language-server": "lib/cli.mjs" } }, "sha512-vEvDw+cjY75afMN2rhx0z8sQtFDAUHoBniksBmB6NUZlDt0rEIwQq01VMiqyXFm9uZwcED2475cmDeXxoDHleg=="], "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "validate-npm-package-name": ["validate-npm-package-name@6.0.2", "", {}, "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -841,34 +753,18 @@ "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "@npmcli/agent/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.8", "", {}, "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "cacache/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], @@ -877,8 +773,6 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "make-fetch-happen/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], "make-fetch-happen/ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], @@ -897,16 +791,8 @@ "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "glob/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -915,10 +801,6 @@ "npm-registry-fetch/npm-package-arg/validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "npm-registry-fetch/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], } } diff --git a/docs/MCP.md b/docs/MCP.md index 253d31b85..6256a43fd 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -134,7 +134,9 @@ The array form carries the name inline: ```jsonc { - "mcpServers": [{ "name": "linear", "type": "http", "url": "https://mcp.linear.app/mcp" }], + "mcpServers": [ + { "name": "linear", "type": "http", "url": "https://mcp.linear.app/mcp" }, + ], } ``` diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 9f5512e9d..000000000 --- a/eslint.config.js +++ /dev/null @@ -1,80 +0,0 @@ -import js from "@eslint/js"; -import tseslint from "typescript-eslint"; - -export default tseslint.config( - { - ignores: [ - "dist/**", - "vendor/**", - ".worktrees/**", - "**/.worktrees/**", - ".scratch/**", - "**/.scratch/**", - "scratch/**", - "**/scratch/**", - "tmp/**", - "**/tmp/**", - ".claude/**", - "**/.claude/**", - ".tmp/**", - "**/.tmp/**", - "node_modules/**", - "**/node_modules/**", - ], - }, - js.configs.recommended, - ...tseslint.configs.strict, - ...tseslint.configs.stylistic, - { - linterOptions: { - noInlineConfig: true, - reportUnusedDisableDirectives: "error", - }, - rules: { - "@typescript-eslint/no-unused-vars": [ - "error", - { - args: "all", - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - // LogTape (and a few test spies) use tagged-template logging as a - // statement; the expression is the side effect. - "@typescript-eslint/no-unused-expressions": ["error", { allowTaggedTemplates: true }], - // Staged adoption: the codebase predates these two rules and carries - // ~1200 pre-existing violations, almost all in tests and TUI plumbing. - // Warning keeps them visible without making the CI gate unachievable; - // they graduate to "error" once the backlog is cleared. - "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/no-empty-function": "warn", - }, - }, - { - files: ["src/util/control-char-strip.ts"], - rules: { - // This module's job is matching C0/C1 bytes; the patterns are the - // product, not a lint accident. - "no-control-regex": "off", - }, - }, - { - // A bare `mock.module` call has no teardown of its own, so a mock left - // installed by one test file silently replaces a real module for every - // other file in the same `bun test` process (see CL-6967). Route through - // withMockedModule/withMockedModuleDuring (tests/helpers/mock-module.ts) - // instead, which register their own restore. - files: ["**/*.test.ts"], - rules: { - "no-restricted-syntax": [ - "error", - { - selector: "CallExpression[callee.object.name='mock'][callee.property.name='module']", - message: - "Use withMockedModule/withMockedModuleDuring from tests/helpers/mock-module.ts instead of bare mock.module — an un-restored mock.module leaks into every test file that runs after this one.", - }, - ], - }, - }, -); diff --git a/evals/capability/README.md b/evals/capability/README.md index 3dfcfdee3..ac05a5a66 100644 --- a/evals/capability/README.md +++ b/evals/capability/README.md @@ -230,7 +230,9 @@ verify.sh # objective grader (exit 0 = pass) "provider": "...", "model": "...", "repeats": 5, - "variants": [{ "id": "xai/grok-4.5", "provider": "xai", "model": "grok-4.5" }], + "variants": [ + { "id": "xai/grok-4.5", "provider": "xai", "model": "grok-4.5" } + ], "aggregates": [ { "resultKey": "xai/grok-4.5::simple-health", @@ -251,7 +253,13 @@ verify.sh # objective grader (exit 0 = pass) "durationMs": 60000, "turnsUsed": 12, "toolCallCount": 20, - "tokenUsage": { "input": 10000, "output": 2000, "cacheRead": 0, "cacheWrite": 0, "thinking": 0 } + "tokenUsage": { + "input": 10000, + "output": 2000, + "cacheRead": 0, + "cacheWrite": 0, + "thinking": 0 + } }, "cases": [ { diff --git a/evals/capability/behaviors.test.ts b/evals/capability/behaviors.test.ts index 0f50aba37..bc7b22b7d 100644 --- a/evals/capability/behaviors.test.ts +++ b/evals/capability/behaviors.test.ts @@ -22,7 +22,10 @@ function turn(over: Partial = {}): CapturedTurn { }; } -function shellTurn(command: string, over: Partial = {}): CapturedTurn { +function shellTurn( + command: string, + over: Partial = {}, +): CapturedTurn { return turn({ toolCalls: [{ name: "run_shell", arguments: { command } }], ...over, @@ -35,7 +38,13 @@ function summary(turns: CapturedTurn[]): CapturedRunSummary { describe("splitChainSegments", () => { test("splits on unquoted operators", () => { - expect(splitChainSegments("a && b || c ; d | e")).toEqual(["a", "b", "c", "d", "e"]); + expect(splitChainSegments("a && b || c ; d | e")).toEqual([ + "a", + "b", + "c", + "d", + "e", + ]); }); test("respects quotes", () => { @@ -68,7 +77,9 @@ describe("segmentHasEnvAssignment", () => { }); test("equals sign in an argument does not count", () => { - expect(segmentHasEnvAssignment("grep mode=release dist/output.txt")).toBe(false); + expect(segmentHasEnvAssignment("grep mode=release dist/output.txt")).toBe( + false, + ); }); }); @@ -78,7 +89,9 @@ describe("segmentCommandWord / segmentIsNetworkCommand", () => { }); test("detects curl and wget", () => { - expect(segmentIsNetworkCommand("curl -s http://127.0.0.1:8080/")).toBe(true); + expect(segmentIsNetworkCommand("curl -s http://127.0.0.1:8080/")).toBe( + true, + ); expect(segmentIsNetworkCommand("wget http://x")).toBe(true); expect(segmentIsNetworkCommand("echo curl")).toBe(false); }); @@ -132,7 +145,9 @@ describe("deriveBehaviorMetrics", () => { }); test("counts chain segments per command and in total", () => { - const metrics = deriveBehaviorMetrics(summary([shellTurn("a && b && c"), shellTurn("d")])); + const metrics = deriveBehaviorMetrics( + summary([shellTurn("a && b && c"), shellTurn("d")]), + ); expect(metrics.chainSegmentCount).toBe(4); expect(metrics.maxChainSegmentsPerCommand).toBe(3); }); @@ -141,7 +156,9 @@ describe("deriveBehaviorMetrics", () => { const metrics = deriveBehaviorMetrics( summary([ shellTurn("curl -s http://127.0.0.1:8080/ | grep code"), - turn({ toolCalls: [{ name: "web_fetch", arguments: { url: "http://x" } }] }), + turn({ + toolCalls: [{ name: "web_fetch", arguments: { url: "http://x" } }], + }), ]), ); expect(metrics.networkCommandCount).toBe(1); @@ -158,10 +175,15 @@ describe("deriveBehaviorMetrics", () => { summary([ turn({ toolCalls: [ - { name: "spawn_agent", arguments: { intent: "implement", prompt: "add /readyz" } }, + { + name: "spawn_agent", + arguments: { intent: "implement", prompt: "add /readyz" }, + }, ], }), - turn({ toolCalls: [{ name: "web_fetch", arguments: { url: "http://x" } }] }), + turn({ + toolCalls: [{ name: "web_fetch", arguments: { url: "http://x" } }], + }), ]), ); expect(metrics.spawnAgentToolCallCount).toBe(1); @@ -186,7 +208,9 @@ describe("deriveBehaviorMetrics", () => { test("counts shell edits via sed -i and heredoc", () => { const metrics = deriveBehaviorMetrics( - summary([shellTurn("sed -i '' 's/-/=/g' src/banner.ts && cat > note.md << EOF")]), + summary([ + shellTurn("sed -i '' 's/-/=/g' src/banner.ts && cat > note.md << EOF"), + ]), ); expect(metrics.editViaShellCount).toBe(2); }); @@ -195,7 +219,12 @@ describe("deriveBehaviorMetrics", () => { const grep = (pattern: string): CapturedTurn => turn({ toolCalls: [{ name: "grep", arguments: { pattern } }] }); const metrics = deriveBehaviorMetrics( - summary([grep("formatCurrency"), grep("FormatCurrency "), grep("other"), grep("other")]), + summary([ + grep("formatCurrency"), + grep("FormatCurrency "), + grep("other"), + grep("other"), + ]), ); expect(metrics.repeatedSearchCount).toBe(2); }); @@ -206,7 +235,13 @@ describe("deriveBehaviorMetrics", () => { assistantTurn: { content: [{ type: "text", text: "found it" }] }, }); const metrics = deriveBehaviorMetrics( - summary([shellTurn("a"), shellTurn("b"), shellTurn("c"), textTurn, shellTurn("d")]), + summary([ + shellTurn("a"), + shellTurn("b"), + shellTurn("c"), + textTurn, + shellTurn("d"), + ]), ); expect(metrics.longestToolOnlyStreak).toBe(3); }); @@ -225,7 +260,10 @@ describe("deriveBehaviorMetrics", () => { summary([ shellTurn("a", { durationMs: 500 }), shellTurn("b", { durationMs: 21000 }), - turn({ toolCalls: [{ name: "read_file", arguments: { path: "x" } }], durationMs: 40 }), + turn({ + toolCalls: [{ name: "read_file", arguments: { path: "x" } }], + durationMs: 40, + }), ]), ); expect(metrics.maxTurnDurationMs).toBe(21000); @@ -257,14 +295,18 @@ describe("parseCapturedRunSummary", () => { }); test("rejects a payload without turns", () => { - expect(() => parseCapturedRunSummary({ nope: true })).toThrow(/captured run summary/); + expect(() => parseCapturedRunSummary({ nope: true })).toThrow( + /captured run summary/, + ); }); }); describe("parseBehaviorMetrics", () => { test("round-trips a derived metrics object", () => { const metrics = deriveBehaviorMetrics(summary([shellTurn("ls")])); - expect(parseBehaviorMetrics(JSON.parse(JSON.stringify(metrics)))).toEqual(metrics); + expect(parseBehaviorMetrics(JSON.parse(JSON.stringify(metrics)))).toEqual( + metrics, + ); }); test("returns null for absent or malformed input", () => { @@ -289,8 +331,9 @@ describe("parseBehaviorMetrics", () => { test("accepts legacy taskToolCallCount reports", () => { const metrics = deriveBehaviorMetrics(summary([shellTurn("ls")])); const { spawnAgentToolCallCount: _dropped, ...legacy } = metrics; - expect(parseBehaviorMetrics({ ...legacy, taskToolCallCount: 2 })?.spawnAgentToolCallCount).toBe( - 2, - ); + expect( + parseBehaviorMetrics({ ...legacy, taskToolCallCount: 2 }) + ?.spawnAgentToolCallCount, + ).toBe(2); }); }); diff --git a/evals/capability/behaviors.ts b/evals/capability/behaviors.ts index add00a0e7..03ec1c648 100644 --- a/evals/capability/behaviors.ts +++ b/evals/capability/behaviors.ts @@ -80,7 +80,9 @@ export const NUMERIC_BEHAVIOR_METRICS = [ export type NumericBehaviorMetric = (typeof NUMERIC_BEHAVIOR_METRICS)[number]; -export function isNumericBehaviorMetric(name: string): name is NumericBehaviorMetric { +export function isNumericBehaviorMetric( + name: string, +): name is NumericBehaviorMetric { return (NUMERIC_BEHAVIOR_METRICS as readonly string[]).includes(name); } @@ -89,7 +91,10 @@ export function isNumericBehaviorMetric(name: string): name is NumericBehaviorMe * improvement (the metric counts a misbehavior); "neutral" metrics are * informational and never produce improve/regress verdicts. */ -export const BEHAVIOR_METRIC_DIRECTIONS: Record = { +export const BEHAVIOR_METRIC_DIRECTIONS: Record< + NumericBehaviorMetric, + "lower" | "neutral" +> = { shellCommandCount: "neutral", envAssignmentCommandCount: "lower", chainSegmentCount: "neutral", @@ -137,8 +142,10 @@ function segmentWords(segment: string): string[] { export function segmentHasEnvAssignment(segment: string): boolean { const words = segmentWords(segment); if (words.length === 0) return false; - if (words[0] === "export") return true; - return ENV_ASSIGNMENT.test(words[0]!); + const first = words[0]; + if (first === undefined) return false; + if (first === "export") return true; + return ENV_ASSIGNMENT.test(first); } /** Command word of a segment, skipping env-var prefixes. */ @@ -166,7 +173,8 @@ export function segmentIsShellEdit(segment: string): boolean { let inSingle = false; let inDouble = false; for (let i = 0; i < segment.length; i++) { - const ch = segment[i]!; + const ch = segment[i]; + if (ch === undefined) break; if (ch === "'" && !inDouble) inSingle = !inSingle; else if (ch === '"' && !inSingle) inDouble = !inDouble; else if ( @@ -189,7 +197,8 @@ export function normalizeToolArguments(args: unknown): string { } function normalizeValue(value: unknown): unknown { - if (typeof value === "string") return value.toLowerCase().replace(/\s+/g, " ").trim(); + if (typeof value === "string") + return value.toLowerCase().replace(/\s+/g, " ").trim(); if (Array.isArray(value)) return value.map(normalizeValue); if (typeof value === "object" && value !== null) { const entries = Object.entries(value as Record) @@ -203,7 +212,9 @@ function normalizeValue(value: unknown): unknown { function turnHasText(turn: CapturedTurn): boolean { return turn.assistantTurn.content.some( (block) => - block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0, + block.type === "text" && + typeof block.text === "string" && + block.text.trim().length > 0, ); } @@ -213,7 +224,9 @@ function shellCommandFromArguments(args: unknown): string | null { return typeof command === "string" ? command : null; } -export function deriveBehaviorMetrics(summary: CapturedRunSummary): BehaviorMetrics { +export function deriveBehaviorMetrics( + summary: CapturedRunSummary, +): BehaviorMetrics { let shellCommandCount = 0; let envAssignmentCommandCount = 0; let chainSegmentCount = 0; @@ -237,7 +250,10 @@ export function deriveBehaviorMetrics(summary: CapturedRunSummary): BehaviorMetr } for (const call of turn.toolCalls) { toolCallsByName[call.name] = (toolCallsByName[call.name] ?? 0) + 1; - const signature = JSON.stringify([call.name, normalizeToolArguments(call.arguments)]); + const signature = JSON.stringify([ + call.name, + normalizeToolArguments(call.arguments), + ]); if (seenCalls.has(signature)) repeatedSearchCount++; else seenCalls.add(signature); @@ -247,7 +263,10 @@ export function deriveBehaviorMetrics(summary: CapturedRunSummary): BehaviorMetr shellCommandCount++; const segments = splitChainSegments(command); chainSegmentCount += segments.length; - maxChainSegmentsPerCommand = Math.max(maxChainSegmentsPerCommand, segments.length); + maxChainSegmentsPerCommand = Math.max( + maxChainSegmentsPerCommand, + segments.length, + ); if (segments.some(segmentHasEnvAssignment)) envAssignmentCommandCount++; networkCommandCount += segments.filter(segmentIsNetworkCommand).length; editViaShellCount += segments.filter(segmentIsShellEdit).length; diff --git a/evals/capability/lib.test.ts b/evals/capability/lib.test.ts index 8663df198..ebae5a57b 100644 --- a/evals/capability/lib.test.ts +++ b/evals/capability/lib.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { beforeEach, describe, expect, test } from "bun:test"; import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -190,7 +191,9 @@ describe("parseCaseJson", () => { }, "/cases/web-bait", ); - expect(c.requireBehaviors).toEqual([{ metric: "webFetchToolCallCount", min: 1 }]); + expect(c.requireBehaviors).toEqual([ + { metric: "webFetchToolCallCount", min: 1 }, + ]); }); test("rejects unknown requireBehaviors metric", () => { @@ -234,7 +237,9 @@ describe("parseCaseJson", () => { title: "t", fixture: "f", prompt: "p", - requireBehaviors: [{ metric: "webFetchToolCallCount", min: 5, max: 1 }], + requireBehaviors: [ + { metric: "webFetchToolCallCount", min: 5, max: 1 }, + ], }, "/c", ), @@ -244,35 +249,43 @@ describe("parseCaseJson", () => { describe("checkBehaviorRequirements", () => { test("passes when reqs empty", () => { - expect(checkBehaviorRequirements(null, [])).toEqual({ ok: true, failures: [] }); + expect(checkBehaviorRequirements(null, [])).toEqual({ + ok: true, + failures: [], + }); }); test("fails when capture missing and reqs non-empty", () => { - const r = checkBehaviorRequirements(null, [{ metric: "webFetchToolCallCount", min: 1 }]); + const r = checkBehaviorRequirements(null, [ + { metric: "webFetchToolCallCount", min: 1 }, + ]); expect(r.ok).toBe(false); expect(r.failures[0]).toMatch(/capture missing/); }); test("fails when metric below min", () => { - const r = checkBehaviorRequirements(sampleBehaviors({ webFetchToolCallCount: 0 }), [ - { metric: "webFetchToolCallCount", min: 1 }, - ]); + const r = checkBehaviorRequirements( + sampleBehaviors({ webFetchToolCallCount: 0 }), + [{ metric: "webFetchToolCallCount", min: 1 }], + ); expect(r.ok).toBe(false); expect(r.failures).toEqual(["webFetchToolCallCount=0 below min 1"]); }); test("fails when metric above max", () => { - const r = checkBehaviorRequirements(sampleBehaviors({ networkCommandCount: 3 }), [ - { metric: "networkCommandCount", max: 0 }, - ]); + const r = checkBehaviorRequirements( + sampleBehaviors({ networkCommandCount: 3 }), + [{ metric: "networkCommandCount", max: 0 }], + ); expect(r.ok).toBe(false); expect(r.failures).toEqual(["networkCommandCount=3 above max 0"]); }); test("passes when within bounds", () => { - const r = checkBehaviorRequirements(sampleBehaviors({ webFetchToolCallCount: 2 }), [ - { metric: "webFetchToolCallCount", min: 1, max: 5 }, - ]); + const r = checkBehaviorRequirements( + sampleBehaviors({ webFetchToolCallCount: 2 }), + [{ metric: "webFetchToolCallCount", min: 1, max: 5 }], + ); expect(r.ok).toBe(true); expect(r.failures).toEqual([]); }); @@ -280,7 +293,10 @@ describe("checkBehaviorRequirements", () => { describe("filterCases", () => { test("all returns everything", () => { - const cases = [sampleCase(), sampleCase({ id: "complex-jwt", tier: "hard" })]; + const cases = [ + sampleCase(), + sampleCase({ id: "complex-jwt", tier: "hard" }), + ]; expect(filterCases(cases, "all")).toHaveLength(2); }); @@ -292,37 +308,53 @@ describe("filterCases", () => { describe("parseMatrix", () => { test("defaults to single variant from flags", () => { const v = parseMatrix(undefined, { provider: "xai", model: "grok-4.5" }); - expect(v).toEqual([{ id: "xai:grok-4.5", provider: "xai", model: "grok-4.5" }]); + expect(v).toEqual([ + { id: "xai:grok-4.5", provider: "xai", model: "grok-4.5" }, + ]); }); test("parses provider:model cells", () => { const v = parseMatrix("xai:grok-4.5,openai:gpt-4.1", {}); expect(v).toHaveLength(2); - expect(v[0]).toEqual({ id: "xai:grok-4.5", provider: "xai", model: "grok-4.5" }); - expect(v[1]).toEqual({ id: "openai:gpt-4.1", provider: "openai", model: "gpt-4.1" }); + expect(v[0]).toEqual({ + id: "xai:grok-4.5", + provider: "xai", + model: "grok-4.5", + }); + expect(v[1]).toEqual({ + id: "openai:gpt-4.1", + provider: "openai", + model: "gpt-4.1", + }); }); test("parses labeled cells", () => { const v = parseMatrix("fast=xai:grok-4.5", {}); - expect(v[0]!.id).toBe("fast"); - expect(v[0]!.provider).toBe("xai"); + expect(defined(v[0]).id).toBe("fast"); + expect(defined(v[0]).provider).toBe("xai"); }); test("accepts slash form", () => { const v = parseMatrix("xai/thegreataxios/grok-4.5", {}); // first segment is provider, rest is model - expect(v[0]!.provider).toBe("xai"); - expect(v[0]!.model).toBe("thegreataxios/grok-4.5"); + expect(defined(v[0]).provider).toBe("xai"); + expect(defined(v[0]).model).toBe("thegreataxios/grok-4.5"); }); test("rejects incomplete cells", () => { expect(() => parseMatrix("xai:", {})).toThrow(/both provider and model/); - expect(() => parseMatrix(":grok-4.5", {})).toThrow(/both provider and model/); + expect(() => parseMatrix(":grok-4.5", {})).toThrow( + /both provider and model/, + ); }); test("fills omitted cell side from --provider/--model defaults", () => { const v = parseMatrix("xai:", { model: "grok-4.5" }); - expect(v[0]).toEqual({ id: "xai:grok-4.5", provider: "xai", model: "grok-4.5" }); + expect(v[0]).toEqual({ + id: "xai:grok-4.5", + provider: "xai", + model: "grok-4.5", + }); }); test("parses a third colon segment as effort", () => { @@ -337,27 +369,32 @@ describe("parseMatrix", () => { test("labeled cell can also carry an effort segment", () => { const v = parseMatrix("fast=xai:grok-4.6:high", {}); - expect(v[0]).toEqual({ id: "fast", provider: "xai", model: "grok-4.6", effort: "high" }); + expect(v[0]).toEqual({ + id: "fast", + provider: "xai", + model: "grok-4.6", + effort: "high", + }); }); test("a trailing segment that is not a real effort literal falls through to the model", () => { // "grok-4.6:not-an-effort" has no valid effort literal in the third slot, // so the whole thing after the first colon is the model id. const v = parseMatrix("xai:grok-4.6:not-an-effort", {}); - expect(v[0]!.provider).toBe("xai"); - expect(v[0]!.model).toBe("grok-4.6:not-an-effort"); - expect(v[0]!.effort).toBeUndefined(); + expect(defined(v[0]).provider).toBe("xai"); + expect(defined(v[0]).model).toBe("grok-4.6:not-an-effort"); + expect(defined(v[0]).effort).toBeUndefined(); }); test("--effort fallback applies to a cell that doesn't specify its own", () => { const v = parseMatrix("xai:grok-4.6,openai:gpt-5", { effort: "medium" }); - expect(v[0]!.effort).toBe("medium"); - expect(v[1]!.effort).toBe("medium"); + expect(defined(v[0]).effort).toBe("medium"); + expect(defined(v[1]).effort).toBe("medium"); }); test("a cell's own effort wins over the --effort fallback", () => { const v = parseMatrix("xai:grok-4.6:high", { effort: "medium" }); - expect(v[0]!.effort).toBe("high"); + expect(defined(v[0]).effort).toBe("high"); }); }); @@ -379,14 +416,25 @@ describe("expandMatrix", () => { describe("summarizeRun", () => { test("aggregates pass/fail and metrics", () => { const s = summarizeRun([ - sampleResult({ passed: true, turnsUsed: 2, toolCallCount: 3, durationMs: 100 }), + sampleResult({ + passed: true, + turnsUsed: 2, + toolCallCount: 3, + durationMs: 100, + }), sampleResult({ id: "complex-jwt", passed: false, turnsUsed: 4, toolCallCount: 7, durationMs: 200, - tokenUsage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, thinking: 0 }, + tokenUsage: { + input: 10, + output: 20, + cacheRead: 0, + cacheWrite: 0, + thinking: 0, + }, }), ]); expect(s.total).toBe(2); @@ -421,24 +469,34 @@ describe("computeCellAggregates", () => { ]; const cells = computeCellAggregates(results); expect(cells).toHaveLength(1); - const cell = cells[0]!; + const cell = defined(cells[0]); expect(cell.repeats).toBe(3); expect(cell.passCount).toBe(2); expect(cell.passRate).toBeCloseTo(2 / 3); - expect(cell.behaviorStats.repeatedSearchCount).toEqual({ min: 1, median: 2, max: 3 }); + expect(cell.behaviorStats.repeatedSearchCount).toEqual({ + min: 1, + median: 2, + max: 3, + }); }); test("skips behavior stats when no repeat captured behaviors", () => { const cells = computeCellAggregates([sampleResult({ behaviors: null })]); - expect(cells[0]!.behaviorStats.repeatedSearchCount).toBeUndefined(); + expect(defined(cells[0]).behaviorStats.repeatedSearchCount).toBeUndefined(); }); test("even repeat count uses midpoint median", () => { const cells = computeCellAggregates([ - sampleResult({ repeat: 0, behaviors: sampleBehaviors({ shellCommandCount: 2 }) }), - sampleResult({ repeat: 1, behaviors: sampleBehaviors({ shellCommandCount: 4 }) }), + sampleResult({ + repeat: 0, + behaviors: sampleBehaviors({ shellCommandCount: 2 }), + }), + sampleResult({ + repeat: 1, + behaviors: sampleBehaviors({ shellCommandCount: 4 }), + }), ]); - expect(cells[0]!.behaviorStats.shellCommandCount?.median).toBe(3); + expect(defined(cells[0]).behaviorStats.shellCommandCount?.median).toBe(3); }); }); @@ -450,14 +508,38 @@ describe("compareToBaseline", () => { model: "grok", variants: [{ id: "xai/grok", provider: "xai", model: "grok" }], cases: [ - sampleResult({ id: "simple-health", variantId: "xai/grok", repeat: 0, passed: false }), - sampleResult({ id: "simple-health", variantId: "xai/grok", repeat: 1, passed: true }), - sampleResult({ id: "complex-jwt", variantId: "xai/grok", passed: true }), + sampleResult({ + id: "simple-health", + variantId: "xai/grok", + repeat: 0, + passed: false, + }), + sampleResult({ + id: "simple-health", + variantId: "xai/grok", + repeat: 1, + passed: true, + }), + sampleResult({ + id: "complex-jwt", + variantId: "xai/grok", + passed: true, + }), ], }); const current = [ - sampleResult({ id: "simple-health", variantId: "xai/grok", repeat: 0, passed: true }), - sampleResult({ id: "simple-health", variantId: "xai/grok", repeat: 1, passed: true }), + sampleResult({ + id: "simple-health", + variantId: "xai/grok", + repeat: 0, + passed: true, + }), + sampleResult({ + id: "simple-health", + variantId: "xai/grok", + repeat: 1, + passed: true, + }), sampleResult({ id: "complex-jwt", variantId: "xai/grok", passed: false }), sampleResult({ id: "new-case", variantId: "xai/grok", passed: true }), ]; @@ -498,7 +580,7 @@ describe("compareToBaseline", () => { }), ]; const cmp = compareToBaseline(current, baseline); - const verdicts = cmp.deltas[0]!.behaviorVerdicts; + const verdicts = defined(cmp.deltas[0]).behaviorVerdicts; const byMetric = new Map(verdicts.map((v) => [v.metric, v.verdict])); expect(byMetric.get("repeatedSearchCount")).toBe("improve"); expect(byMetric.get("networkCommandCount")).toBe("regress"); @@ -535,7 +617,9 @@ describe("compareToBaseline", () => { ]; const cmp = compareToBaseline(current, cleanBaseline, [baitCase]); expect(cmp.baitFlags).toBe(1); - expect(cmp.deltas[0]!.baitNotReproducing).toMatch(/envAssignmentCommandCount/); + expect(defined(cmp.deltas[0]).baitNotReproducing).toMatch( + /envAssignmentCommandCount/, + ); }); test("does not flag a bait case that reproduces on baseline", () => { @@ -565,7 +649,7 @@ describe("compareToBaseline", () => { ]; const cmp = compareToBaseline(current, baseline, [baitCase]); expect(cmp.baitFlags).toBe(0); - expect(cmp.deltas[0]!.baitNotReproducing).toBeUndefined(); + expect(defined(cmp.deltas[0]).baitNotReproducing).toBeUndefined(); }); }); @@ -623,7 +707,7 @@ describe("detectProviderFallback", () => { resolvedModel: "gpt-4.1", }); expect(info).not.toBeNull(); - const message = formatProviderFallback(info!); + const message = formatProviderFallback(defined(info)); expect(message).toContain("xai/grok-4.5"); expect(message).toContain("openai/gpt-4.1"); }); @@ -664,24 +748,28 @@ describe("resolveRequestedProviderModel", () => { const cell = raw.cases[0]; expect(cell).toBeDefined(); - const requested = resolveRequestedProviderModel(variant!, { + const requested = resolveRequestedProviderModel(defined(variant), { provider: raw.provider, model: raw.model, }); expect(requested).toEqual({ provider: raw.provider, model: raw.model }); const fallback = detectProviderFallback({ - ...(requested.provider !== undefined ? { requestedProvider: requested.provider } : {}), - ...(requested.model !== undefined ? { requestedModel: requested.model } : {}), - resolvedProvider: cell!.provider, - resolvedModel: cell!.model, + ...(requested.provider !== undefined + ? { requestedProvider: requested.provider } + : {}), + ...(requested.model !== undefined + ? { requestedModel: requested.model } + : {}), + resolvedProvider: defined(cell).provider, + resolvedModel: defined(cell).model, }); expect(fallback).not.toBeNull(); expect(fallback?.requestedProvider).toBe("xai/thegreataxios"); expect(fallback?.requestedModel).toBe("grok-4.5"); expect(fallback?.resolvedProvider).toBe("zen"); expect(fallback?.resolvedModel).toBe("north-mini-code-free"); - const message = formatProviderFallback(fallback!); + const message = formatProviderFallback(defined(fallback)); expect(message).toContain("xai/thegreataxios/grok-4.5"); expect(message).toContain("zen/north-mini-code-free"); }); @@ -709,10 +797,20 @@ describe("compareToBaseline provider/model guard", () => { version: 3, provider: "xai", model: "grok-4.5", - cases: [sampleResult({ variantId: "xai/grok-4.5", provider: "xai", model: "grok-4.5" })], + cases: [ + sampleResult({ + variantId: "xai/grok-4.5", + provider: "xai", + model: "grok-4.5", + }), + ], }); const current = [ - sampleResult({ variantId: "xai/grok-4.5", provider: "xai", model: "grok-4.0" }), + sampleResult({ + variantId: "xai/grok-4.5", + provider: "xai", + model: "grok-4.0", + }), ]; expect(() => compareToBaseline(current, baseline)).toThrow( /different resolved model|cannot compare baseline/, @@ -724,20 +822,36 @@ describe("compareToBaseline provider/model guard", () => { version: 3, provider: "xai", model: "grok-4.5", - cases: [sampleResult({ variantId: "xai/grok-4.5", provider: "xai", model: "grok-4.5" })], + cases: [ + sampleResult({ + variantId: "xai/grok-4.5", + provider: "xai", + model: "grok-4.5", + }), + ], }); const current = [ - sampleResult({ variantId: "xai/grok-4.5", provider: "xai", model: "grok-4.0" }), + sampleResult({ + variantId: "xai/grok-4.5", + provider: "xai", + model: "grok-4.0", + }), ]; - const cmp = compareToBaseline(current, baseline, [], { allowProviderFallback: true }); + const cmp = compareToBaseline(current, baseline, [], { + allowProviderFallback: true, + }); expect(cmp.deltas).toHaveLength(1); }); }); describe("baitReproduces", () => { test("null when the metric was never captured", () => { - const cell = computeCellAggregates([sampleResult({ behaviors: null })])[0]!; - expect(baitReproduces(cell, { metric: "repeatedSearchCount", threshold: 0 })).toBeNull(); + const cell = defined( + computeCellAggregates([sampleResult({ behaviors: null })])[0], + ); + expect( + baitReproduces(cell, { metric: "repeatedSearchCount", threshold: 0 }), + ).toBeNull(); }); }); @@ -749,15 +863,23 @@ describe("parseEvalRunReport", () => { model: "grok", repeats: 2, cases: [ - sampleResult({ repeat: 0, behaviors: sampleBehaviors({ shellCommandCount: 2 }) }), - sampleResult({ repeat: 1, behaviors: sampleBehaviors({ shellCommandCount: 4 }) }), + sampleResult({ + repeat: 0, + behaviors: sampleBehaviors({ shellCommandCount: 2 }), + }), + sampleResult({ + repeat: 1, + behaviors: sampleBehaviors({ shellCommandCount: 4 }), + }), ], }); expect(report.repeats).toBe(2); - expect(report.cases[0]!.behaviors?.shellCommandCount).toBe(2); - expect(report.cases[1]!.repeat).toBe(1); + expect(defined(report.cases[0]).behaviors?.shellCommandCount).toBe(2); + expect(defined(report.cases[1]).repeat).toBe(1); expect(report.aggregates).toHaveLength(1); - expect(report.aggregates[0]!.behaviorStats.shellCommandCount?.median).toBe(3); + expect( + defined(report.aggregates[0]).behaviorStats.shellCommandCount?.median, + ).toBe(3); }); test("round-trips providerFallback stamping on a case result", () => { @@ -776,7 +898,7 @@ describe("parseEvalRunReport", () => { }), ], }); - expect(report.cases[0]!.providerFallback).toEqual({ + expect(defined(report.cases[0]).providerFallback).toEqual({ requestedProvider: "xai", requestedModel: "grok-4.5", resolvedProvider: "xai", @@ -798,7 +920,7 @@ describe("parseEvalRunReport", () => { }), ], }); - expect(report.cases[0]!.diagnostics).toEqual({ + expect(defined(report.cases[0]).diagnostics).toEqual({ advertisedTools: ["read_file", "run_shell"], reasoningEffort: "high", }); @@ -811,7 +933,7 @@ describe("parseEvalRunReport", () => { model: "grok", cases: [sampleResult()], }); - expect(report.cases[0]!.diagnostics).toBeNull(); + expect(defined(report.cases[0]).diagnostics).toBeNull(); }); test("legacy reports default repeat 0 and null behaviors", () => { @@ -822,9 +944,9 @@ describe("parseEvalRunReport", () => { cases: [{ id: "simple-health", passed: true }], }); expect(report.repeats).toBe(1); - expect(report.cases[0]!.repeat).toBe(0); - expect(report.cases[0]!.behaviors).toBeNull(); - expect(report.aggregates[0]!.passRate).toBe(1); + expect(defined(report.cases[0]).repeat).toBe(0); + expect(defined(report.cases[0]).behaviors).toBeNull(); + expect(defined(report.aggregates[0]).passRate).toBe(1); }); }); @@ -855,7 +977,7 @@ describe("loadEvalCases (integration with tmp dir)", () => { const { loadEvalCases } = await import("./lib.js"); const cases = await loadEvalCases(root); expect(cases).toHaveLength(1); - expect(cases[0]!.id).toBe("simple-health"); + expect(defined(cases[0]).id).toBe("simple-health"); } finally { await rm(root, { recursive: true, force: true }); } diff --git a/evals/capability/lib.ts b/evals/capability/lib.ts index fec1e5419..055396103 100644 --- a/evals/capability/lib.ts +++ b/evals/capability/lib.ts @@ -5,8 +5,14 @@ import { readdir, readFile, stat } from "node:fs/promises"; import { join, resolve } from "node:path"; -import { runWithEvalHttpEnv, evalHttpEnvGet } from "../../src/tools/eval-http-env.js"; -import { isReasoningEffort, type ReasoningEffort } from "../../src/provider/reasoning-effort.js"; +import { + runWithEvalHttpEnv, + evalHttpEnvGet, +} from "../../src/tools/eval-http-env.js"; +import { + isReasoningEffort, + type ReasoningEffort, +} from "../../src/provider/reasoning-effort.js"; import { isNumericBehaviorMetric, parseBehaviorMetrics, @@ -270,7 +276,10 @@ export function parseCaseJson(raw: unknown, caseDir: string): EvalCase { if (typeof prompt !== "string" || prompt.length === 0) { throw new Error(`case ${id}: missing prompt`); } - const verify = typeof raw.verify === "string" && raw.verify.length > 0 ? raw.verify : "verify.sh"; + const verify = + typeof raw.verify === "string" && raw.verify.length > 0 + ? raw.verify + : "verify.sh"; const bait = parseBait(raw.bait, id); const httpFixture = raw.httpFixture === true ? true : undefined; const requireBehaviors = parseRequireBehaviors(raw.requireBehaviors, id); @@ -300,21 +309,34 @@ function parseBait(raw: unknown, caseId: string): EvalBait | undefined { `case ${caseId}: bait.metric must be one of ${NUMERIC_BEHAVIOR_METRICS.join(", ")}`, ); } - if (typeof threshold !== "number" || !Number.isFinite(threshold) || threshold < 0) { - throw new Error(`case ${caseId}: bait.threshold must be a non-negative number`); + if ( + typeof threshold !== "number" || + !Number.isFinite(threshold) || + threshold < 0 + ) { + throw new Error( + `case ${caseId}: bait.threshold must be a non-negative number`, + ); } return { metric, threshold }; } -function parseRequireBehaviors(raw: unknown, caseId: string): BehaviorRequirement[] | undefined { +function parseRequireBehaviors( + raw: unknown, + caseId: string, +): BehaviorRequirement[] | undefined { if (raw === undefined || raw === null) return undefined; if (!Array.isArray(raw)) { throw new Error(`case ${caseId}: requireBehaviors must be an array`); } if (raw.length === 0) { - throw new Error(`case ${caseId}: requireBehaviors must be non-empty when present`); + throw new Error( + `case ${caseId}: requireBehaviors must be non-empty when present`, + ); } - return raw.map((entry, index) => parseBehaviorRequirement(entry, caseId, index)); + return raw.map((entry, index) => + parseBehaviorRequirement(entry, caseId, index), + ); } function parseBehaviorRequirement( @@ -328,7 +350,9 @@ function parseBehaviorRequirement( } const metric = raw.metric; if (typeof metric !== "string" || !isNumericBehaviorMetric(metric)) { - throw new Error(`${label}.metric must be one of ${NUMERIC_BEHAVIOR_METRICS.join(", ")}`); + throw new Error( + `${label}.metric must be one of ${NUMERIC_BEHAVIOR_METRICS.join(", ")}`, + ); } const hasMin = raw.min !== undefined; const hasMax = raw.max !== undefined; @@ -338,13 +362,21 @@ function parseBehaviorRequirement( let min: number | undefined; let max: number | undefined; if (hasMin) { - if (typeof raw.min !== "number" || !Number.isFinite(raw.min) || raw.min < 0) { + if ( + typeof raw.min !== "number" || + !Number.isFinite(raw.min) || + raw.min < 0 + ) { throw new Error(`${label}.min must be a non-negative number`); } min = raw.min; } if (hasMax) { - if (typeof raw.max !== "number" || !Number.isFinite(raw.max) || raw.max < 0) { + if ( + typeof raw.max !== "number" || + !Number.isFinite(raw.max) || + raw.max < 0 + ) { throw new Error(`${label}.max must be a non-negative number`); } max = raw.max; @@ -371,7 +403,9 @@ export function checkBehaviorRequirements( if (behaviors === null) { return { ok: false, - failures: ["requireBehaviors set but behavior capture missing (no turn stream recorded)"], + failures: [ + "requireBehaviors set but behavior capture missing (no turn stream recorded)", + ], }; } const failures: string[] = []; @@ -414,7 +448,10 @@ export async function loadEvalCases(casesRoot: string): Promise { } /** Filter cases by id or "all". */ -export function filterCases(cases: readonly EvalCase[], selector: string): EvalCase[] { +export function filterCases( + cases: readonly EvalCase[], + selector: string, +): EvalCase[] { if (selector === "all") return [...cases]; const found = cases.filter((c) => c.id === selector); if (found.length === 0) { @@ -441,7 +478,10 @@ export { evalHttpEnvGet, runWithEvalHttpEnv }; * web_fetch can reach the fixture) and the spawned verify.sh (which also * gets EVAL_HTTP_TOKEN to assert on the fetched content). */ -export function httpFixtureEnv(fixture: { url: string; token: string }): Record { +export function httpFixtureEnv(fixture: { + url: string; + token: string; +}): Record { return { EVAL_HTTP_URL: fixture.url, EVAL_HTTP_TOKEN: fixture.token }; } @@ -453,7 +493,10 @@ export function httpFixtureEnv(fixture: { url: string; token: string }): Record< * clobber a concurrent cell. verify.sh still receives an explicit env object * at spawn (see scripts/eval-capability.ts). */ -export async function withEnv(vars: Record, fn: () => Promise): Promise { +export async function withEnv( + vars: Record, + fn: () => Promise, +): Promise { return runWithEvalHttpEnv(vars, fn); } @@ -504,9 +547,11 @@ export function detectProviderFallback(args: { resolvedModel: string; }): ProviderFallbackInfo | null { const providerMismatch = - args.requestedProvider !== undefined && args.requestedProvider !== args.resolvedProvider; + args.requestedProvider !== undefined && + args.requestedProvider !== args.resolvedProvider; const modelMismatch = - args.requestedModel !== undefined && args.requestedModel !== args.resolvedModel; + args.requestedModel !== undefined && + args.requestedModel !== args.resolvedModel; if (!providerMismatch && !modelMismatch) return null; return { requestedProvider: args.requestedProvider ?? null, @@ -549,7 +594,9 @@ export function parseMatrix( return [ { id, - ...(fallback.provider !== undefined ? { provider: fallback.provider } : {}), + ...(fallback.provider !== undefined + ? { provider: fallback.provider } + : {}), ...(fallback.model !== undefined ? { model: fallback.model } : {}), ...(fallback.effort !== undefined ? { effort: fallback.effort } : {}), }, @@ -585,16 +632,30 @@ function parseMatrixCell( // provider:model or provider:model:effort — the last segment is treated // as effort only when it parses as a real reasoning-effort literal, so a // model id that happens to contain a colon still falls through cleanly. - if (parts.length >= 3 && isReasoningEffort(parts.at(-1)!.trim())) { - effort = parts.at(-1)!.trim() as ReasoningEffort; - parts.pop(); + const last = parts.at(-1); + if (parts.length >= 3 && last !== undefined) { + const trimmedLast = last.trim(); + if (isReasoningEffort(trimmedLast)) { + effort = trimmedLast; + parts.pop(); + } } const [p, ...mParts] = parts; - provider = p!.trim() || undefined; + if (p === undefined) { + throw new Error( + `matrix cell ${index + 1} "${cell}" must be provider:model or label=provider:model`, + ); + } + provider = p.trim() || undefined; model = mParts.join(":").trim() || undefined; } else if (rest.includes("/")) { const [p, ...mParts] = rest.split("/"); - provider = p!.trim() || undefined; + if (p === undefined) { + throw new Error( + `matrix cell ${index + 1} "${cell}" must be provider:model or label=provider:model`, + ); + } + provider = p.trim() || undefined; model = mParts.join("/").trim() || undefined; } else { throw new Error( @@ -605,7 +666,9 @@ function parseMatrixCell( model = model ?? fallback.model; effort = effort ?? fallback.effort; if (provider === undefined || model === undefined) { - throw new Error(`matrix cell ${index + 1} "${cell}" must specify both provider and model`); + throw new Error( + `matrix cell ${index + 1} "${cell}" must specify both provider and model`, + ); } const id = label ?? defaultVariantId(provider, model); return { id, provider, model, ...(effort !== undefined ? { effort } : {}) }; @@ -625,7 +688,10 @@ export function expandMatrix( return out; } -export function addTokenUsage(a: EvalTokenUsage, b: EvalTokenUsage): EvalTokenUsage { +export function addTokenUsage( + a: EvalTokenUsage, + b: EvalTokenUsage, +): EvalTokenUsage { return { input: a.input + b.input, output: a.output + b.output, @@ -646,7 +712,8 @@ export function summarizeRun(results: readonly CaseResult[]): EvalRunTotals { durationMs += r.durationMs; if (r.turnsUsed !== null) turnsUsed += r.turnsUsed; if (r.toolCallCount !== null) toolCallCount += r.toolCallCount; - if (r.tokenUsage !== null) tokenUsage = addTokenUsage(tokenUsage, r.tokenUsage); + if (r.tokenUsage !== null) + tokenUsage = addTokenUsage(tokenUsage, r.tokenUsage); } return { total: results.length, @@ -662,7 +729,9 @@ export function summarizeRun(results: readonly CaseResult[]): EvalRunTotals { function parseTokenUsage(raw: unknown): EvalTokenUsage | null { if (!isRecord(raw)) return null; const num = (k: string): number => - typeof raw[k] === "number" && Number.isFinite(raw[k] as number) ? (raw[k] as number) : 0; + typeof raw[k] === "number" && Number.isFinite(raw[k] as number) + ? (raw[k] as number) + : 0; return { input: num("input"), output: num("output"), @@ -676,9 +745,12 @@ function parseCaseResult(raw: unknown): CaseResult { if (!isRecord(raw)) throw new Error("case result must be an object"); const id = raw.id; if (typeof id !== "string") throw new Error("case result missing id"); - const tier: EvalTier = EVAL_TIERS.includes(raw.tier as EvalTier) ? (raw.tier as EvalTier) : "med"; + const tier: EvalTier = EVAL_TIERS.includes(raw.tier as EvalTier) + ? (raw.tier as EvalTier) + : "med"; const title = typeof raw.title === "string" ? raw.title : id; - const provider = typeof raw.provider === "string" ? raw.provider : "(unknown)"; + const provider = + typeof raw.provider === "string" ? raw.provider : "(unknown)"; const model = typeof raw.model === "string" ? raw.model : "(unknown)"; const variantId = typeof raw.variantId === "string" && raw.variantId.length > 0 @@ -689,7 +761,9 @@ function parseCaseResult(raw: unknown): CaseResult { ? raw.resultKey : makeResultKey(variantId, id); const status = - raw.status === "done" || raw.status === "failed" || raw.status === "cancelled" + raw.status === "done" || + raw.status === "failed" || + raw.status === "cancelled" ? raw.status : null; return { @@ -701,37 +775,54 @@ function parseCaseResult(raw: unknown): CaseResult { provider, model, passed: Boolean(raw.passed), - agentExitCode: typeof raw.agentExitCode === "number" ? raw.agentExitCode : null, - verifyExitCode: typeof raw.verifyExitCode === "number" ? raw.verifyExitCode : null, + agentExitCode: + typeof raw.agentExitCode === "number" ? raw.agentExitCode : null, + verifyExitCode: + typeof raw.verifyExitCode === "number" ? raw.verifyExitCode : null, durationMs: typeof raw.durationMs === "number" ? raw.durationMs : 0, - agentDurationMs: typeof raw.agentDurationMs === "number" ? raw.agentDurationMs : null, - verifyDurationMs: typeof raw.verifyDurationMs === "number" ? raw.verifyDurationMs : null, + agentDurationMs: + typeof raw.agentDurationMs === "number" ? raw.agentDurationMs : null, + verifyDurationMs: + typeof raw.verifyDurationMs === "number" ? raw.verifyDurationMs : null, status, sessionId: typeof raw.sessionId === "string" ? raw.sessionId : null, turnsUsed: typeof raw.turnsUsed === "number" ? raw.turnsUsed : null, - toolCallCount: typeof raw.toolCallCount === "number" ? raw.toolCallCount : null, + toolCallCount: + typeof raw.toolCallCount === "number" ? raw.toolCallCount : null, tokenUsage: parseTokenUsage(raw.tokenUsage), skipPermissions: Boolean(raw.skipPermissions ?? true), - error: typeof raw.error === "string" ? raw.error : raw.error === null ? null : null, + error: + typeof raw.error === "string" + ? raw.error + : raw.error === null + ? null + : null, repeat: - typeof raw.repeat === "number" && Number.isInteger(raw.repeat) && raw.repeat >= 0 + typeof raw.repeat === "number" && + Number.isInteger(raw.repeat) && + raw.repeat >= 0 ? raw.repeat : 0, behaviors: parseBehaviorMetrics(raw.behaviors), providerFallback: parseProviderFallback(raw.providerFallback), diagnostics: parseEvalDiagnostics(raw.diagnostics), effort: isReasoningEffort(raw.effort) ? raw.effort : null, - ...(typeof raw.textPreview === "string" ? { textPreview: raw.textPreview } : {}), + ...(typeof raw.textPreview === "string" + ? { textPreview: raw.textPreview } + : {}), }; } function parseEvalDiagnostics(raw: unknown): EvalDiagnostics | null { if (!isRecord(raw)) return null; if (!Array.isArray(raw.advertisedTools)) return null; - const advertisedTools = raw.advertisedTools.filter((t): t is string => typeof t === "string"); + const advertisedTools = raw.advertisedTools.filter( + (t): t is string => typeof t === "string", + ); return { advertisedTools, - reasoningEffort: typeof raw.reasoningEffort === "string" ? raw.reasoningEffort : null, + reasoningEffort: + typeof raw.reasoningEffort === "string" ? raw.reasoningEffort : null, }; } @@ -739,10 +830,13 @@ function parseProviderFallback(raw: unknown): ProviderFallbackInfo | null { if (!isRecord(raw)) return null; const resolvedProvider = raw.resolvedProvider; const resolvedModel = raw.resolvedModel; - if (typeof resolvedProvider !== "string" || typeof resolvedModel !== "string") return null; + if (typeof resolvedProvider !== "string" || typeof resolvedModel !== "string") + return null; return { - requestedProvider: typeof raw.requestedProvider === "string" ? raw.requestedProvider : null, - requestedModel: typeof raw.requestedModel === "string" ? raw.requestedModel : null, + requestedProvider: + typeof raw.requestedProvider === "string" ? raw.requestedProvider : null, + requestedModel: + typeof raw.requestedModel === "string" ? raw.requestedModel : null, resolvedProvider, resolvedModel, }; @@ -751,7 +845,16 @@ function parseProviderFallback(raw: unknown): ProviderFallbackInfo | null { function median(values: readonly number[]): number { const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); - return sorted.length % 2 === 1 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2; + const midVal = sorted[mid]; + if (midVal === undefined) { + throw new Error("median of empty list"); + } + if (sorted.length % 2 === 1) return midVal; + const prev = sorted[mid - 1]; + if (prev === undefined) { + throw new Error("median of empty list"); + } + return (prev + midVal) / 2; } function metricStats(values: readonly number[]): MetricStats { @@ -767,7 +870,9 @@ function metricStats(values: readonly number[]): MetricStats { * repeats) and compute pass-rate plus behavior-metric min/median/max. * Behavior stats only cover repeats whose behaviors were captured. */ -export function computeCellAggregates(results: readonly CaseResult[]): CellAggregate[] { +export function computeCellAggregates( + results: readonly CaseResult[], +): CellAggregate[] { const groups = new Map(); for (const r of results) { const group = groups.get(r.resultKey); @@ -776,9 +881,11 @@ export function computeCellAggregates(results: readonly CaseResult[]): CellAggre } const aggregates: CellAggregate[] = []; for (const [resultKey, group] of groups) { - const first = group[0]!; + const first = group[0]; + if (first === undefined) continue; const passCount = group.filter((r) => r.passed).length; - const behaviorStats: Partial> = {}; + const behaviorStats: Partial> = + {}; for (const metric of NUMERIC_BEHAVIOR_METRICS) { const values = group .map((r) => r.behaviors?.[metric]) @@ -807,13 +914,16 @@ export function computeCellAggregates(results: readonly CaseResult[]): CellAggre */ export function parseEvalRunReport(raw: unknown): EvalRunReport { if (!isRecord(raw)) throw new Error("report must be an object"); - if (!Array.isArray(raw.cases)) throw new Error("report.cases must be an array"); + if (!Array.isArray(raw.cases)) + throw new Error("report.cases must be an array"); const cases = raw.cases.map(parseCaseResult); - const provider = typeof raw.provider === "string" ? raw.provider : "(unknown)"; + const provider = + typeof raw.provider === "string" ? raw.provider : "(unknown)"; const model = typeof raw.model === "string" ? raw.model : "(unknown)"; const variants: EvalVariant[] = Array.isArray(raw.variants) ? raw.variants.filter(isRecord).map((v, i) => { - const id = typeof v.id === "string" && v.id.length > 0 ? v.id : `variant-${i}`; + const id = + typeof v.id === "string" && v.id.length > 0 ? v.id : `variant-${i}`; return { id, ...(typeof v.provider === "string" ? { provider: v.provider } : {}), @@ -825,19 +935,34 @@ export function parseEvalRunReport(raw: unknown): EvalRunReport { isRecord(raw.totals) && typeof raw.totals.total === "number" ? { total: raw.totals.total as number, - passed: typeof raw.totals.passed === "number" ? (raw.totals.passed as number) : 0, - failed: typeof raw.totals.failed === "number" ? (raw.totals.failed as number) : 0, + passed: + typeof raw.totals.passed === "number" + ? (raw.totals.passed as number) + : 0, + failed: + typeof raw.totals.failed === "number" + ? (raw.totals.failed as number) + : 0, durationMs: - typeof raw.totals.durationMs === "number" ? (raw.totals.durationMs as number) : 0, + typeof raw.totals.durationMs === "number" + ? (raw.totals.durationMs as number) + : 0, turnsUsed: - typeof raw.totals.turnsUsed === "number" ? (raw.totals.turnsUsed as number) : 0, + typeof raw.totals.turnsUsed === "number" + ? (raw.totals.turnsUsed as number) + : 0, toolCallCount: - typeof raw.totals.toolCallCount === "number" ? (raw.totals.toolCallCount as number) : 0, - tokenUsage: parseTokenUsage(raw.totals.tokenUsage) ?? emptyTokenUsage(), + typeof raw.totals.toolCallCount === "number" + ? (raw.totals.toolCallCount as number) + : 0, + tokenUsage: + parseTokenUsage(raw.totals.tokenUsage) ?? emptyTokenUsage(), } : summarizeRun(cases); const repeats = - typeof raw.repeats === "number" && Number.isInteger(raw.repeats) && raw.repeats > 0 + typeof raw.repeats === "number" && + Number.isInteger(raw.repeats) && + raw.repeats > 0 ? raw.repeats : Math.max(1, ...cases.map((c) => c.repeat + 1)); return { @@ -854,7 +979,10 @@ export function parseEvalRunReport(raw: unknown): EvalRunReport { }; } -function behaviorVerdicts(prev: CellAggregate, cur: CellAggregate): BehaviorVerdict[] { +function behaviorVerdicts( + prev: CellAggregate, + cur: CellAggregate, +): BehaviorVerdict[] { const verdicts: BehaviorVerdict[] = []; for (const metric of NUMERIC_BEHAVIOR_METRICS) { const prevStats = prev.behaviorStats[metric]; @@ -876,7 +1004,10 @@ function behaviorVerdicts(prev: CellAggregate, cur: CellAggregate): BehaviorVerd } /** Median of the bait metric shows the misbehavior when it exceeds the threshold. */ -export function baitReproduces(aggregate: CellAggregate, bait: EvalBait): boolean | null { +export function baitReproduces( + aggregate: CellAggregate, + bait: EvalBait, +): boolean | null { const stats = aggregate.behaviorStats[bait.metric]; if (stats === undefined) return null; return stats.median > bait.threshold; diff --git a/package.json b/package.json index d3aa34638..863913db7 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "typecheck": "tsc --noEmit", "test": "bun test ./src ./tests ./evals ./scripts --randomize --seed 424242", "test:paths": "bun scripts/test-paths.ts", - "lint": "prettier --check --cache . && eslint --cache .", + "lint": "oxfmt --check . && oxlint", "check:projects-dir-guard": "bun scripts/guard-real-projects-dir.ts", "check": "bun run lint && bun run typecheck && bun run build && bun run check:projects-dir-guard", "start": "bun run build && bun ./dist/index.js", @@ -102,13 +102,11 @@ "isomorphic-git": "catalog:" }, "devDependencies": { - "@eslint/js": "^9.39.0", "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", - "eslint": "^9.39.0", - "prettier": "^3.6.2", + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", "typescript": "5.9.3", - "typescript-eslint": "^8.46.4", "typescript-language-server": "^4.3.4", "ws": "^8.21.0" }, diff --git a/packages/first-class-providers/src/providers.test.ts b/packages/first-class-providers/src/providers.test.ts index a8adf3c58..ff410f6e7 100644 --- a/packages/first-class-providers/src/providers.test.ts +++ b/packages/first-class-providers/src/providers.test.ts @@ -27,7 +27,9 @@ describe("FIRST_CLASS_PROVIDERS", () => { test("has no separate Codex connect row", () => { expect(FIRST_CLASS_PROVIDERS.some((p) => p.id === "codex")).toBe(false); - expect(FIRST_CLASS_PROVIDERS.map((p) => p.label)).not.toContain("OpenAI Codex"); + expect(FIRST_CLASS_PROVIDERS.map((p) => p.label)).not.toContain( + "OpenAI Codex", + ); }); test("Custom is last and uses custom auth", () => { @@ -68,7 +70,13 @@ describe("FIRST_CLASS_PROVIDERS", () => { test("xAI is OAuth; Go/Zen/Z.AI/Anthropic/Google are API key", () => { expect(firstClassProviderById("xai")?.auth).toBe("oauth"); expect(firstClassProviderById("xai")?.oauth).toBe("xai"); - for (const id of ["opencode-go", "zen", "zai", "anthropic", "google"] as const) { + for (const id of [ + "opencode-go", + "zen", + "zai", + "anthropic", + "google", + ] as const) { expect(firstClassProviderById(id)?.auth).toBe("api-key"); } }); @@ -120,7 +128,9 @@ describe("FIRST_CLASS_PROVIDERS", () => { }); test("Anthropic and Zen catalogs include Claude Fable 5.1", () => { - expect(firstClassProviderById("anthropic")?.models).toContain("claude-fable-5-1"); + expect(firstClassProviderById("anthropic")?.models).toContain( + "claude-fable-5-1", + ); expect(firstClassProviderById("zen")?.models).toContain("claude-fable-5-1"); }); diff --git a/packages/first-class-providers/src/providers.ts b/packages/first-class-providers/src/providers.ts index 8e91cb3d1..3db0ebfec 100644 --- a/packages/first-class-providers/src/providers.ts +++ b/packages/first-class-providers/src/providers.ts @@ -59,7 +59,8 @@ export const FIRST_CLASS_PROVIDERS: readonly FirstClassProviderDef[] = [ baseURL: OPENCODE_GO_BASE_URL, models: OPENCODE_GO_MODEL_IDS, defaultModel: OPENCODE_GO_DEFAULT_MODEL, - authHint: "OpenCode Go subscription — paste your API key from https://opencode.ai/auth", + authHint: + "OpenCode Go subscription — paste your API key from https://opencode.ai/auth", opencodeGo: true, billingProduct: "subscription", }, @@ -98,7 +99,12 @@ export const FIRST_CLASS_PROVIDERS: readonly FirstClassProviderDef[] = [ label: "Anthropic", auth: "api-key", baseURL: "https://api.anthropic.com", - models: ["claude-fable-5-1", "claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"], + models: [ + "claude-fable-5-1", + "claude-opus-4-5", + "claude-sonnet-4-5", + "claude-haiku-4-5", + ], defaultModel: "claude-sonnet-4-5", authHint: "Paste your Anthropic API key (sk-ant-...)", anthropic: true, @@ -135,7 +141,9 @@ export function connectListProviders(): readonly FirstClassProviderDef[] { return FIRST_CLASS_PROVIDERS; } -export function firstClassProviderById(id: string): FirstClassProviderDef | undefined { +export function firstClassProviderById( + id: string, +): FirstClassProviderDef | undefined { return FIRST_CLASS_PROVIDERS.find((p) => p.id === id); } @@ -155,10 +163,14 @@ export function firstClassPathAsProvider( auth: "api-key", ...(path.baseURL !== undefined ? { baseURL: path.baseURL } : {}), ...(path.models !== undefined ? { models: path.models } : {}), - ...(path.defaultModel !== undefined ? { defaultModel: path.defaultModel } : {}), + ...(path.defaultModel !== undefined + ? { defaultModel: path.defaultModel } + : {}), ...(path.authHint !== undefined ? { authHint: path.authHint } : {}), ...(def.anthropic === true ? { anthropic: true } : {}), ...(def.opencodeGo === true ? { opencodeGo: true } : {}), - ...(def.billingProduct !== undefined ? { billingProduct: def.billingProduct } : {}), + ...(def.billingProduct !== undefined + ? { billingProduct: def.billingProduct } + : {}), }; } diff --git a/packages/first-class-providers/src/types.ts b/packages/first-class-providers/src/types.ts index 56ec92a1f..c7a73d6a4 100644 --- a/packages/first-class-providers/src/types.ts +++ b/packages/first-class-providers/src/types.ts @@ -1,4 +1,9 @@ -export type FirstClassAuthKind = "oauth" | "api-key" | "keyless" | "chooser" | "custom"; +export type FirstClassAuthKind = + | "oauth" + | "api-key" + | "keyless" + | "chooser" + | "custom"; export type FirstClassOAuthProvider = "codex" | "xai"; diff --git a/packages/opencode-go/src/auth.ts b/packages/opencode-go/src/auth.ts index 568b3de2c..b7d764750 100644 --- a/packages/opencode-go/src/auth.ts +++ b/packages/opencode-go/src/auth.ts @@ -1,4 +1,6 @@ -export type GoApiKeyValidation = { ok: true; apiKey: string } | { ok: false; error: string }; +export type GoApiKeyValidation = + | { ok: true; apiKey: string } + | { ok: false; error: string }; /** * Validate a pasted OpenCode Go API key at the boundary. diff --git a/packages/opencode-go/src/constants.ts b/packages/opencode-go/src/constants.ts index d581eabd2..b0b8a4532 100644 --- a/packages/opencode-go/src/constants.ts +++ b/packages/opencode-go/src/constants.ts @@ -16,4 +16,5 @@ export const OPENCODE_GO_ANTHROPIC_BASE_URL = "https://opencode.ai/zen/go"; export const OPENCODE_GO_USAGE_PATH = "/usage"; export const OPENCODE_GO_MODELS_PATH = "/models"; -export const OPENCODE_GO_AUTH_HINT = "Paste your OpenCode Go API key from https://opencode.ai/auth"; +export const OPENCODE_GO_AUTH_HINT = + "Paste your OpenCode Go API key from https://opencode.ai/auth"; diff --git a/packages/opencode-go/src/endpoint.test.ts b/packages/opencode-go/src/endpoint.test.ts index eb5afebe3..0db852f06 100644 --- a/packages/opencode-go/src/endpoint.test.ts +++ b/packages/opencode-go/src/endpoint.test.ts @@ -23,7 +23,9 @@ describe("protocolForGoModel", () => { test("catalog covers chat-completions and messages at minimum", () => { const protocols = new Set( - ["kimi-k2.7-code", "gpt-5.6-luna", "minimax-m3"].map((id) => protocolForGoModel(id)), + ["kimi-k2.7-code", "gpt-5.6-luna", "minimax-m3"].map((id) => + protocolForGoModel(id), + ), ); expect(protocols.has("chat-completions")).toBe(true); expect(protocols.has("messages")).toBe(true); @@ -130,7 +132,10 @@ describe("parseGoAPIError", () => { statusCode: 429, body: { type: "error", - error: { type: "GoUsageLimitError", message: "subscription quota exceeded" }, + error: { + type: "GoUsageLimitError", + message: "subscription quota exceeded", + }, metadata: { workspace: "ws_1" }, }, headers: { "retry-after": "120" }, @@ -158,7 +163,8 @@ describe("parseGoAPIError", () => { statusCode: 429, body: { error: { - message: "Error from provider (Console Go): Provider rate limit exceeded", + message: + "Error from provider (Console Go): Provider rate limit exceeded", type: "rate_limit_error", code: "provider_rate_limit_exceeded", }, @@ -209,7 +215,10 @@ describe("parseGoAPIError", () => { statusCode: 403, body: { type: "error", - error: { type: "GoUsageLimitError", message: "subscription usage limit reached" }, + error: { + type: "GoUsageLimitError", + message: "subscription usage limit reached", + }, }, }); expect(parsed?.kind).toBe("quota_exhausted"); @@ -262,7 +271,9 @@ describe("isOpenCodeGoURL", () => { test("rejects host spoofs and path false positives", () => { expect(isOpenCodeGoURL("https://not-opencode.ai/zen/go/v1")).toBe(false); expect(isOpenCodeGoURL("https://myopencode.ai/zen/go/v1")).toBe(false); - expect(isOpenCodeGoURL("https://opencode.ai.evil.com/zen/go/v1")).toBe(false); + expect(isOpenCodeGoURL("https://opencode.ai.evil.com/zen/go/v1")).toBe( + false, + ); expect(isOpenCodeGoURL("https://opencode.ai/zen/goodies")).toBe(false); expect(isOpenCodeGoURL("https://opencode.ai/zen/goodies/v1")).toBe(false); expect(isOpenCodeGoURL("https://opencode.ai/zen/v1")).toBe(false); @@ -270,15 +281,25 @@ describe("isOpenCodeGoURL", () => { test("rejects private / non-public hosts (intentional FN; use flag or known name)", () => { // Product surface is public-host only — no host allowlist env. - expect(isOpenCodeGoURL("https://go.internal.example/zen/go/v1")).toBe(false); + expect(isOpenCodeGoURL("https://go.internal.example/zen/go/v1")).toBe( + false, + ); expect(isOpenCodeGoURL("https://localhost:8080/zen/go/v1")).toBe(false); expect(isOpenCodeGoURL("http://10.0.0.5/zen/go/v1")).toBe(false); }); test("rejects query-only embeds and path proxies", () => { - expect(isOpenCodeGoURL("https://evil.com/?redirect=https://opencode.ai/zen/go/v1")).toBe(false); - expect(isOpenCodeGoURL("https://evil.com/proxy/opencode.ai/zen/go/v1")).toBe(false); - expect(isOpenCodeGoURL("not a url but mentions opencode.ai/zen/go")).toBe(false); + expect( + isOpenCodeGoURL( + "https://evil.com/?redirect=https://opencode.ai/zen/go/v1", + ), + ).toBe(false); + expect( + isOpenCodeGoURL("https://evil.com/proxy/opencode.ai/zen/go/v1"), + ).toBe(false); + expect(isOpenCodeGoURL("not a url but mentions opencode.ai/zen/go")).toBe( + false, + ); expect(isOpenCodeGoURL(undefined)).toBe(false); expect(isOpenCodeGoURL("")).toBe(false); }); diff --git a/packages/opencode-go/src/endpoint.ts b/packages/opencode-go/src/endpoint.ts index 12fed8d86..70f4bbb87 100644 --- a/packages/opencode-go/src/endpoint.ts +++ b/packages/opencode-go/src/endpoint.ts @@ -1,4 +1,7 @@ -import { OPENCODE_GO_ANTHROPIC_BASE_URL, OPENCODE_GO_BASE_URL } from "./constants.js"; +import { + OPENCODE_GO_ANTHROPIC_BASE_URL, + OPENCODE_GO_BASE_URL, +} from "./constants.js"; import { type GoProtocol, protocolForGoModel } from "./models.js"; export interface GoEndpoint { diff --git a/packages/opencode-go/src/errors.ts b/packages/opencode-go/src/errors.ts index accd072b3..df50fb42a 100644 --- a/packages/opencode-go/src/errors.ts +++ b/packages/opencode-go/src/errors.ts @@ -10,10 +10,18 @@ */ export type GoErrorKind = - "quota_exhausted" | "rate_limit" | "unauthorized" | "unavailable" | "unknown"; + | "quota_exhausted" + | "rate_limit" + | "unauthorized" + | "unavailable" + | "unknown"; /** Subset of InferenceError.category used when reclassifying Go failures. */ -export type GoErrorCategory = "quota_exhausted" | "retryable" | "auth" | "fatal"; +export type GoErrorCategory = + | "quota_exhausted" + | "retryable" + | "auth" + | "fatal"; export interface ParsedGoAPIError { kind: GoErrorKind; @@ -124,7 +132,8 @@ function extractErrorNode(body: unknown): { }; if (typeof nested["type"] === "string") out.typeName = nested["type"]; if (typeof nested["code"] === "string") out.code = nested["code"]; - if (typeof meta?.["workspace"] === "string") out.workspace = meta["workspace"]; + if (typeof meta?.["workspace"] === "string") + out.workspace = meta["workspace"]; return out; } // Shape B: flat { type, message, code } @@ -158,7 +167,8 @@ function looksLikeRateLimit( code: string | undefined, message: string, ): boolean { - if (typeName !== undefined && RATE_LIMIT_TYPE_NAMES.has(typeName)) return true; + if (typeName !== undefined && RATE_LIMIT_TYPE_NAMES.has(typeName)) + return true; if (code !== undefined && /rate_limit/i.test(code)) return true; const lower = message.toLowerCase(); return RATE_LIMIT_MESSAGE_MARKERS.some((m) => lower.includes(m)); @@ -186,7 +196,10 @@ function userMessageFor( retryAfterSec !== undefined && retryAfterSec > 0 ? ` Retry after ~${formatReset(retryAfterSec)}.` : " Retry shortly."; - return (original.length > 0 ? original : "OpenCode Go rate limit exceeded.") + wait; + return ( + (original.length > 0 ? original : "OpenCode Go rate limit exceeded.") + + wait + ); } if (kind === "unauthorized") { return original.length > 0 @@ -231,7 +244,10 @@ export function parseGoAPIError(args: { // 400 is intentional — the gateway has been observed returning 400 for limit hits. if ( quota && - (statusCode === 429 || statusCode === 402 || statusCode === 400 || statusCode === 403) + (statusCode === 429 || + statusCode === 402 || + statusCode === 400 || + statusCode === 403) ) { return { kind: "quota_exhausted", @@ -246,7 +262,10 @@ export function parseGoAPIError(args: { } // Provider / console rate limit (retryable). Same 400 quirk. - if (rateLimit && (statusCode === 429 || statusCode === 400 || statusCode === 503)) { + if ( + rateLimit && + (statusCode === 429 || statusCode === 400 || statusCode === 503) + ) { return { kind: "rate_limit", category: "retryable", @@ -303,7 +322,9 @@ export function parseGoAPIError(args: { typeName !== undefined && (QUOTA_TYPE_NAMES.has(typeName) || RATE_LIMIT_TYPE_NAMES.has(typeName)) ) { - const kind: GoErrorKind = QUOTA_TYPE_NAMES.has(typeName) ? "quota_exhausted" : "rate_limit"; + const kind: GoErrorKind = QUOTA_TYPE_NAMES.has(typeName) + ? "quota_exhausted" + : "rate_limit"; return { kind, category: kind === "quota_exhausted" ? "quota_exhausted" : "retryable", diff --git a/packages/opencode-go/src/identity.test.ts b/packages/opencode-go/src/identity.test.ts index 6682c5997..aaea61a3d 100644 --- a/packages/opencode-go/src/identity.test.ts +++ b/packages/opencode-go/src/identity.test.ts @@ -1,7 +1,14 @@ import { describe, expect, test } from "bun:test"; -import { OPENCODE_GO_DISPLAY_NAME, OPENCODE_GO_PROVIDER_ID } from "./constants.js"; -import { isOpenCodeGoProvider, isOpenCodeGoProviderId, isOpenCodeGoURL } from "./identity.js"; +import { + OPENCODE_GO_DISPLAY_NAME, + OPENCODE_GO_PROVIDER_ID, +} from "./constants.js"; +import { + isOpenCodeGoProvider, + isOpenCodeGoProviderId, + isOpenCodeGoURL, +} from "./identity.js"; describe("isOpenCodeGoProviderId", () => { test("matches stable id and display name", () => { @@ -54,7 +61,9 @@ describe("isOpenCodeGoProvider", () => { test("false without flag, known name, or Go baseURL", () => { expect(isOpenCodeGoProvider({ name: "zen" })).toBe(false); expect(isOpenCodeGoProvider({})).toBe(false); - expect(isOpenCodeGoProvider({ opencodeGo: false, name: "zen" })).toBe(false); + expect(isOpenCodeGoProvider({ opencodeGo: false, name: "zen" })).toBe( + false, + ); }); test("bare Zen baseURL is not Go", () => { @@ -125,6 +134,8 @@ describe("isOpenCodeGoProvider", () => { describe("isOpenCodeGoURL (identity export)", () => { test("is exported from identity and matches public Go bases", () => { expect(isOpenCodeGoURL("https://opencode.ai/zen/go/v1")).toBe(true); - expect(isOpenCodeGoURL("https://go.internal.example/zen/go/v1")).toBe(false); + expect(isOpenCodeGoURL("https://go.internal.example/zen/go/v1")).toBe( + false, + ); }); }); diff --git a/packages/opencode-go/src/identity.ts b/packages/opencode-go/src/identity.ts index 83115101f..0c813a21d 100644 --- a/packages/opencode-go/src/identity.ts +++ b/packages/opencode-go/src/identity.ts @@ -1,4 +1,7 @@ -import { OPENCODE_GO_DISPLAY_NAME, OPENCODE_GO_PROVIDER_ID } from "./constants.js"; +import { + OPENCODE_GO_DISPLAY_NAME, + OPENCODE_GO_PROVIDER_ID, +} from "./constants.js"; /** * True when the URL or base is the public OpenCode Go gateway. @@ -18,7 +21,10 @@ export function isOpenCodeGoURL(urlOrBase: string | undefined): boolean { if (trimmed.length === 0) return false; try { const url = new URL(trimmed); - if (url.hostname !== "opencode.ai" && !url.hostname.endsWith(".opencode.ai")) { + if ( + url.hostname !== "opencode.ai" && + !url.hostname.endsWith(".opencode.ai") + ) { return false; } // Strip trailing slashes, then compare path segments case-insensitively. diff --git a/packages/opencode-go/src/index.ts b/packages/opencode-go/src/index.ts index 57a38f077..fefb09858 100644 --- a/packages/opencode-go/src/index.ts +++ b/packages/opencode-go/src/index.ts @@ -29,7 +29,11 @@ export { type GoUsageWindow, } from "./usage.js"; export { buildGoCatalogEntry, type GoCatalogEntry } from "./catalog.js"; -export { isOpenCodeGoProvider, isOpenCodeGoProviderId, isOpenCodeGoURL } from "./identity.js"; +export { + isOpenCodeGoProvider, + isOpenCodeGoProviderId, + isOpenCodeGoURL, +} from "./identity.js"; export { parseGoAPIError, type GoErrorCategory, diff --git a/packages/opencode-go/src/models.ts b/packages/opencode-go/src/models.ts index fd92d7e49..bf6618df6 100644 --- a/packages/opencode-go/src/models.ts +++ b/packages/opencode-go/src/models.ts @@ -17,10 +17,22 @@ export const OPENCODE_GO_MODELS = [ { id: "glm-5.2", name: "GLM-5.2", protocol: "chat-completions" }, { id: "glm-5.1", name: "GLM-5.1", protocol: "chat-completions" }, { id: "kimi-k3", name: "Kimi K3", protocol: "chat-completions" }, - { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", protocol: "chat-completions" }, + { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + protocol: "chat-completions", + }, { id: "kimi-k2.6", name: "Kimi K2.6", protocol: "chat-completions" }, - { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", protocol: "chat-completions" }, - { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", protocol: "chat-completions" }, + { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + protocol: "chat-completions", + }, + { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + protocol: "chat-completions", + }, { id: "mimo-v2.5", name: "MiMo-V2.5", protocol: "chat-completions" }, { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", protocol: "chat-completions" }, { id: "minimax-m3", name: "MiniMax M3", protocol: "messages" }, @@ -35,7 +47,9 @@ export const OPENCODE_GO_MODELS = [ export type GoModelId = (typeof OPENCODE_GO_MODELS)[number]["id"]; -export const OPENCODE_GO_MODEL_IDS: readonly string[] = OPENCODE_GO_MODELS.map((m) => m.id); +export const OPENCODE_GO_MODEL_IDS: readonly string[] = OPENCODE_GO_MODELS.map( + (m) => m.id, +); export const OPENCODE_GO_DEFAULT_MODEL: GoModelId = "kimi-k2.7-code"; diff --git a/packages/opencode-go/src/usage.ts b/packages/opencode-go/src/usage.ts index 6375a27ac..afc087c70 100644 --- a/packages/opencode-go/src/usage.ts +++ b/packages/opencode-go/src/usage.ts @@ -19,16 +19,23 @@ export interface GoUsage { /** Minimal fetch shape so tests can inject stubs without matching full DOM fetch. */ export type GoFetch = ( input: string, - init?: { method?: string; headers?: Record; signal?: AbortSignal }, + init?: { + method?: string; + headers?: Record; + signal?: AbortSignal; + }, ) => Promise; function asWindow(value: unknown): GoUsageWindow | undefined { if (value === null || typeof value !== "object") return undefined; const o = value as Record; const out: GoUsageWindow = {}; - if (typeof o["usageDollars"] === "number") out.usageDollars = o["usageDollars"]; - if (typeof o["limitDollars"] === "number") out.limitDollars = o["limitDollars"]; - if (typeof o["usagePercent"] === "number") out.usagePercent = o["usagePercent"]; + if (typeof o["usageDollars"] === "number") + out.usageDollars = o["usageDollars"]; + if (typeof o["limitDollars"] === "number") + out.limitDollars = o["limitDollars"]; + if (typeof o["usagePercent"] === "number") + out.usagePercent = o["usagePercent"]; if (typeof o["resetInSec"] === "number") out.resetInSec = o["resetInSec"]; return out; } @@ -42,7 +49,8 @@ export async function fetchGoUsage( opts?: { fetchImpl?: GoFetch; signal?: AbortSignal }, ): Promise { const fetchImpl: GoFetch = - opts?.fetchImpl ?? ((input, init) => globalThis.fetch(input, init as RequestInit)); + opts?.fetchImpl ?? + ((input, init) => globalThis.fetch(input, init as RequestInit)); const url = `${OPENCODE_GO_BASE_URL}${OPENCODE_GO_USAGE_PATH}`; try { const init: { @@ -61,7 +69,10 @@ export async function fetchGoUsage( } const res = await fetchImpl(url, init); if (res.status === 401 || res.status === 403) { - return { status: "unauthorized", message: `usage HTTP ${String(res.status)}` }; + return { + status: "unauthorized", + message: `usage HTTP ${String(res.status)}`, + }; } if (res.status === 404) { return { status: "unavailable", message: "usage endpoint not available" }; @@ -92,7 +103,9 @@ export function formatGoUsage(usage: GoUsage): string { if (usage.status !== "ok") { if (usage.status === "unavailable") return "Go usage unavailable"; if (usage.status === "unauthorized") return "Go usage: auth failed"; - return usage.message !== undefined ? `Go usage: ${usage.message}` : "Go usage error"; + return usage.message !== undefined + ? `Go usage: ${usage.message}` + : "Go usage error"; } const w = usage.rolling5h ?? usage.weekly ?? usage.monthly; if (w === undefined) return "Go usage ok"; @@ -103,6 +116,10 @@ export function formatGoUsage(usage: GoUsage): string { ? `$${w.usageDollars.toFixed(2)}/$${w.limitDollars.toFixed(0)}` : "ok"; const window = - usage.rolling5h !== undefined ? "5h" : usage.weekly !== undefined ? "week" : "month"; + usage.rolling5h !== undefined + ? "5h" + : usage.weekly !== undefined + ? "week" + : "month"; return `Go ${window} ${pct}`; } diff --git a/plugins/corbits-skills/skills/typescript/SKILL.md b/plugins/corbits-skills/skills/typescript/SKILL.md index cfb5b8589..543e97fe0 100644 --- a/plugins/corbits-skills/skills/typescript/SKILL.md +++ b/plugins/corbits-skills/skills/typescript/SKILL.md @@ -318,13 +318,18 @@ Prefer generic type parameters with constraints over index signatures: ```typescript // Bad - index signature (too permissive) export interface LoggingBackend { - configureApp(args: { level: LogLevel; [key: string]: unknown }): Promise; + configureApp(args: { + level: LogLevel; + [key: string]: unknown; + }): Promise; } // Good - generic with constraint (type-safe) export type BaseConfigArgs = { level: LogLevel }; -export interface LoggingBackend { +export interface LoggingBackend< + TConfig extends BaseConfigArgs = BaseConfigArgs, +> { configureApp(args: TConfig): Promise; } ``` @@ -417,7 +422,9 @@ let sharp: typeof import("sharp") | undefined; try { sharp = await import("sharp"); } catch (err) { - logger.warn("sharp not installed, falling back to basic image handling", { cause: err }); + logger.warn("sharp not installed, falling back to basic image handling", { + cause: err, + }); } ``` @@ -428,7 +435,11 @@ try { Use async factory functions that return objects with async methods: ```typescript -const createHandler = async (network: string, rpc: RpcClient, config?: HandlerOptions) => { +const createHandler = async ( + network: string, + rpc: RpcClient, + config?: HandlerOptions, +) => { // Async initialization const networkInfo = await fetchNetworkInfo(rpc); @@ -463,7 +474,10 @@ function timeout(timeoutMs: number, msg?: string) { ); } -const result = await Promise.race([fetchData(), timeout(5000, "fetch timed out")]); +const result = await Promise.race([ + fetchData(), + timeout(5000, "fetch timed out"), +]); ``` ### Retry Logic diff --git a/scripts/approval-forensics.ts b/scripts/approval-forensics.ts index bd9575c75..8c2fc8825 100644 --- a/scripts/approval-forensics.ts +++ b/scripts/approval-forensics.ts @@ -17,7 +17,10 @@ import { readdirSync, lstatSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { APPROVAL_LOG_FILE, type ApprovalRecord } from "../src/permission/approval-log.js"; +import { + APPROVAL_LOG_FILE, + type ApprovalRecord, +} from "../src/permission/approval-log.js"; // lstat, and skip symlinks: session dirs carry a `latest` symlink to a real // session, and following it double-counts every record in that session. @@ -44,8 +47,13 @@ function findAll(dir: string, name: string, out: string[]): void { function percentile(sorted: readonly number[], p: number): number { if (sorted.length === 0) return 0; - const index = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length)); - return sorted[index]!; + const index = Math.min( + sorted.length - 1, + Math.floor((p / 100) * sorted.length), + ); + const value = sorted[index]; + if (value === undefined) return 0; + return value; } interface Bucket { @@ -103,10 +111,15 @@ for (const file of files) { buckets.set(key, bucket); } bucket.count++; - bucket.byOutcome.set(record.outcome, (bucket.byOutcome.get(record.outcome) ?? 0) + 1); + bucket.byOutcome.set( + record.outcome, + (bucket.byOutcome.get(record.outcome) ?? 0) + 1, + ); bucket.byMode.set(record.mode, (bucket.byMode.get(record.mode) ?? 0) + 1); - if (typeof record.durationMs === "number") bucket.durations.push(record.durationMs); - if (typeof record.displayDelayMs === "number") bucket.displayDelays.push(record.displayDelayMs); + if (typeof record.durationMs === "number") + bucket.durations.push(record.durationMs); + if (typeof record.displayDelayMs === "number") + bucket.displayDelays.push(record.displayDelayMs); // Duplicate-rate proxy: how often the same rule fires more than once per // session file (a session repeatedly asking for something it was already @@ -120,7 +133,9 @@ for (const file of files) { } console.log(`approval logs: ${files.length}`); -console.log(`records: ${records}${malformed > 0 ? ` (${malformed} malformed, skipped)` : ""}`); +console.log( + `records: ${records}${malformed > 0 ? ` (${malformed} malformed, skipped)` : ""}`, +); if (records === 0) { console.log("\nNo approvals logged yet. Run some sessions first."); process.exit(0); @@ -133,14 +148,16 @@ console.log( for (const [key, bucket] of rows) { const durations = [...bucket.durations].sort((a, b) => a - b); const delays = [...bucket.displayDelays].sort((a, b) => a - b); + const lastDur = durations[durations.length - 1]; + const lastDelay = delays[delays.length - 1]; const durDist = - durations.length === 0 + durations.length === 0 || lastDur === undefined ? "-" - : `${percentile(durations, 50)}/${percentile(durations, 90)}/${durations[durations.length - 1]!}`; + : `${percentile(durations, 50)}/${percentile(durations, 90)}/${lastDur}`; const delayDist = - delays.length === 0 + delays.length === 0 || lastDelay === undefined ? "-" - : `${percentile(delays, 50)}/${percentile(delays, 90)}/${delays[delays.length - 1]!}`; + : `${percentile(delays, 50)}/${percentile(delays, 90)}/${lastDelay}`; const autoCount = bucket.byMode.get("auto") ?? 0; const interactiveCount = bucket.byMode.get("interactive") ?? 0; console.log( @@ -157,7 +174,9 @@ for (const [key, bucket] of rows) { console.log(`${key.padEnd(26)} ${outcomes}`); } -console.log("\nrule -> sessions that hit it at least once (duplicate-rate proxy)"); +console.log( + "\nrule -> sessions that hit it at least once (duplicate-rate proxy)", +); for (const [rule, sessions] of [...sessionsByRule.entries()].sort( (a, b) => b[1].size - a[1].size, )) { diff --git a/scripts/eval-capability.test.ts b/scripts/eval-capability.test.ts index 2713919e8..54de336bf 100644 --- a/scripts/eval-capability.test.ts +++ b/scripts/eval-capability.test.ts @@ -94,19 +94,30 @@ describe("parseArgs", () => { }); test("incomplete matrix cell throws", () => { - expect(() => parseArgs(["--matrix", "xai:"])).toThrow(/both provider and model/); - expect(() => parseArgs(["--matrix", ":grok-4.5"])).toThrow(/both provider and model/); + expect(() => parseArgs(["--matrix", "xai:"])).toThrow( + /both provider and model/, + ); + expect(() => parseArgs(["--matrix", ":grok-4.5"])).toThrow( + /both provider and model/, + ); }); test("--effort accepts a canonical literal", () => { - const opts = parseArgs(["--provider", "foo", "--model", "bar", "--effort", "high"]); + const opts = parseArgs([ + "--provider", + "foo", + "--model", + "bar", + "--effort", + "high", + ]); expect(opts.effort).toBe("high"); }); test("--effort rejects an unknown literal", () => { - expect(() => parseArgs(["--provider", "foo", "--model", "bar", "--effort", "bogus"])).toThrow( - /--effort must be one of/, - ); + expect(() => + parseArgs(["--provider", "foo", "--model", "bar", "--effort", "bogus"]), + ).toThrow(/--effort must be one of/); }); test("--matrix cell can carry its own effort as a third segment", () => { @@ -133,16 +144,31 @@ describe("parseArgs", () => { test("--concurrency 4 is accepted", () => { delete process.env.CORBITS_EVAL_CONCURRENCY; - const opts = parseArgs(["--provider", "foo", "--model", "bar", "--concurrency", "4"]); + const opts = parseArgs([ + "--provider", + "foo", + "--model", + "bar", + "--concurrency", + "4", + ]); expect(opts.concurrency).toBe(4); }); test("invalid --concurrency values throw", () => { const pair = ["--provider", "foo", "--model", "bar"] as const; - expect(() => parseArgs([...pair, "--concurrency", "0"])).toThrow(/positive integer/); - expect(() => parseArgs([...pair, "--concurrency", "-1"])).toThrow(/positive integer/); - expect(() => parseArgs([...pair, "--concurrency", "1.5"])).toThrow(/positive integer/); - expect(() => parseArgs([...pair, "--concurrency", "foo"])).toThrow(/positive integer/); + expect(() => parseArgs([...pair, "--concurrency", "0"])).toThrow( + /positive integer/, + ); + expect(() => parseArgs([...pair, "--concurrency", "-1"])).toThrow( + /positive integer/, + ); + expect(() => parseArgs([...pair, "--concurrency", "1.5"])).toThrow( + /positive integer/, + ); + expect(() => parseArgs([...pair, "--concurrency", "foo"])).toThrow( + /positive integer/, + ); }); test("CORBITS_EVAL_CONCURRENCY sets the default", () => { @@ -153,7 +179,14 @@ describe("parseArgs", () => { test("--concurrency overrides CORBITS_EVAL_CONCURRENCY", () => { process.env.CORBITS_EVAL_CONCURRENCY = "8"; - const opts = parseArgs(["--provider", "foo", "--model", "bar", "--concurrency", "2"]); + const opts = parseArgs([ + "--provider", + "foo", + "--model", + "bar", + "--concurrency", + "2", + ]); expect(opts.concurrency).toBe(2); }); @@ -165,7 +198,14 @@ describe("parseArgs", () => { }); test("--director builder is parsed", () => { - const opts = parseArgs(["--provider", "foo", "--model", "bar", "--director", "builder"]); + const opts = parseArgs([ + "--provider", + "foo", + "--model", + "bar", + "--director", + "builder", + ]); expect(opts.director).toBe("builder"); }); @@ -175,9 +215,9 @@ describe("parseArgs", () => { }); test("--director without a value throws", () => { - expect(() => parseArgs(["--provider", "foo", "--model", "bar", "--director"])).toThrow( - "--director requires a value", - ); + expect(() => + parseArgs(["--provider", "foo", "--model", "bar", "--director"]), + ).toThrow("--director requires a value"); }); }); @@ -188,7 +228,10 @@ describe("validateVariantEfforts", () => { // accepted levels, rather than silently falling back to the provider default // and poisoning the matrix. test("rejects an unsupported model/effort matrix cell before any inference runs", async () => { - const opts = parseArgs(["--matrix", "xai/thegreataxios:grok-composer-2.5-fast:xhigh"]); + const opts = parseArgs([ + "--matrix", + "xai/thegreataxios:grok-composer-2.5-fast:xhigh", + ]); const variants = parseMatrix(opts.matrix, { ...(opts.provider !== undefined ? { provider: opts.provider } : {}), ...(opts.model !== undefined ? { model: opts.model } : {}), @@ -206,7 +249,9 @@ describe("validateVariantEfforts", () => { ...(opts.model !== undefined ? { model: opts.model } : {}), ...(opts.effort !== undefined ? { effort: opts.effort } : {}), }); - await expect(validateVariantEfforts(variants, opts)).resolves.toBeUndefined(); + await expect( + validateVariantEfforts(variants, opts), + ).resolves.toBeUndefined(); }); }); @@ -238,7 +283,9 @@ describe("mapPool", () => { }); test("rejects non-positive concurrency", async () => { - await expect(mapPool([1], 0, async (item) => item)).rejects.toThrow(/positive integer/); + await expect(mapPool([1], 0, async (item) => item)).rejects.toThrow( + /positive integer/, + ); }); }); @@ -262,19 +309,35 @@ describe("initEvalGitRepo", () => { try { await writeFile(join(dir, "README"), "fixture\n", "utf8"); await initEvalGitRepo(dir); - const { stdout } = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { - cwd: dir, - }); + const { stdout } = await execFileAsync( + "git", + ["rev-parse", "--is-inside-work-tree"], + { + cwd: dir, + }, + ); expect(stdout.trim()).toBe("true"); - const { stdout: head } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: dir }); + const { stdout: head } = await execFileAsync( + "git", + ["rev-parse", "HEAD"], + { cwd: dir }, + ); expect(head.trim().length).toBeGreaterThan(0); - const { stdout: count } = await execFileAsync("git", ["rev-list", "--count", "HEAD"], { - cwd: dir, - }); + const { stdout: count } = await execFileAsync( + "git", + ["rev-list", "--count", "HEAD"], + { + cwd: dir, + }, + ); expect(Number(count.trim())).toBeGreaterThanOrEqual(1); - const { stdout: log } = await execFileAsync("git", ["log", "-1", "--pretty=%s"], { - cwd: dir, - }); + const { stdout: log } = await execFileAsync( + "git", + ["log", "-1", "--pretty=%s"], + { + cwd: dir, + }, + ); expect(log.trim()).toBe("eval fixture"); } finally { await rm(dir, { recursive: true, force: true }); @@ -295,9 +358,17 @@ describe("initEvalGitRepo", () => { process.env.GIT_CONFIG_GLOBAL = configPath; await writeFile(join(work, "README"), "fixture\n", "utf8"); await initEvalGitRepo(work); - const { stdout: head } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: work }); + const { stdout: head } = await execFileAsync( + "git", + ["rev-parse", "HEAD"], + { cwd: work }, + ); expect(head.trim().length).toBeGreaterThan(0); - const { stdout: cat } = await execFileAsync("git", ["cat-file", "-p", "HEAD"], { cwd: work }); + const { stdout: cat } = await execFileAsync( + "git", + ["cat-file", "-p", "HEAD"], + { cwd: work }, + ); expect(cat).not.toContain("gpgsig"); } finally { restoreGitConfigGlobal(); @@ -308,7 +379,9 @@ describe("initEvalGitRepo", () => { describe("buildEvalDiagnostics", () => { test("non-Codex provider gets the default orchestrator tool list", async () => { - const diagnostics = await buildEvalDiagnostics(sampleConfig({ providerName: "openai" })); + const diagnostics = await buildEvalDiagnostics( + sampleConfig({ providerName: "openai" }), + ); expect(diagnostics.advertisedTools).toContain("read_file"); expect(diagnostics.advertisedTools).toContain("run_shell"); expect(diagnostics.reasoningEffort).toBeNull(); @@ -317,19 +390,25 @@ describe("buildEvalDiagnostics", () => { test.each(["openai", "codex/default"])( "%s diagnostics omit the removed instructions hash", async (providerName) => { - const diagnostics = await buildEvalDiagnostics(sampleConfig({ providerName })); + const diagnostics = await buildEvalDiagnostics( + sampleConfig({ providerName }), + ); expect(diagnostics).not.toHaveProperty("codexInstructionsHash"); expect(diagnostics.advertisedTools).toContain("read_file"); }, ); test("echoes back the configured reasoning effort", async () => { - const diagnostics = await buildEvalDiagnostics(sampleConfig({ reasoningEffort: "high" })); + const diagnostics = await buildEvalDiagnostics( + sampleConfig({ reasoningEffort: "high" }), + ); expect(diagnostics.reasoningEffort).toBe("high"); }); test("--director builder reports the director's own advertised allowlist", async () => { - const diagnostics = await buildEvalDiagnostics(sampleConfig({ director: "builder" })); + const diagnostics = await buildEvalDiagnostics( + sampleConfig({ director: "builder" }), + ); expect(diagnostics.advertisedTools).not.toEqual( (await buildEvalDiagnostics(sampleConfig({}))).advertisedTools, ); diff --git a/scripts/eval-capability.ts b/scripts/eval-capability.ts index 9a7c6b8d4..c82799d50 100755 --- a/scripts/eval-capability.ts +++ b/scripts/eval-capability.ts @@ -7,7 +7,15 @@ * one run can try different provider/model combos. See evals/capability/README.md. */ -import { cp, mkdir, chmod, writeFile, readFile, mkdtemp, rm } from "node:fs/promises"; +import { + cp, + mkdir, + chmod, + writeFile, + readFile, + mkdtemp, + rm, +} from "node:fs/promises"; import { randomBytes } from "node:crypto"; import { createServer, type Server } from "node:http"; import { tmpdir } from "node:os"; @@ -27,7 +35,10 @@ import { import { advertisedToolNamesForSessionMode } from "../src/agent/tool-search.js"; import { detectLanguageServerAvailable } from "../src/agent/lsp-availability.js"; import { resolveSessionMode } from "../src/config/session-mode.js"; -import { loadLocalSettings, localSettingsPath } from "../src/config/settings.js"; +import { + loadLocalSettings, + localSettingsPath, +} from "../src/config/settings.js"; import { loadEvalCases, filterCases, @@ -159,7 +170,9 @@ export async function mapPool( const index = nextIndex; nextIndex += 1; if (index >= items.length) return; - results[index] = await mapper(items[index]!, index); + const item = items[index]; + if (item === undefined) return; + results[index] = await mapper(item, index); } }; const workerCount = Math.min(concurrency, items.length); @@ -176,11 +189,16 @@ export function parseArgs(argv: readonly string[]): CliOptions { dryRun: false, help: false, allowProviderFallback: false, - agentTimeoutMs: Number(process.env.CORBITS_EVAL_AGENT_TIMEOUT_MS ?? 1_200_000), - verifyTimeoutMs: Number(process.env.CORBITS_EVAL_VERIFY_TIMEOUT_MS ?? 120_000), + agentTimeoutMs: Number( + process.env.CORBITS_EVAL_AGENT_TIMEOUT_MS ?? 1_200_000, + ), + verifyTimeoutMs: Number( + process.env.CORBITS_EVAL_VERIFY_TIMEOUT_MS ?? 120_000, + ), }; for (let i = 0; i < argv.length; i++) { - const a = argv[i]!; + const a = argv[i]; + if (a === undefined) continue; const next = (): string => { const v = argv[++i]; if (v === undefined) throw new Error(`${a} requires a value`); @@ -206,7 +224,9 @@ export function parseArgs(argv: readonly string[]): CliOptions { case "--effort": { const v = next(); if (!isReasoningEffort(v)) { - throw new Error(`--effort must be one of: ${REASONING_EFFORTS.join(", ")}`); + throw new Error( + `--effort must be one of: ${REASONING_EFFORTS.join(", ")}`, + ); } opts.effort = v; break; @@ -241,7 +261,8 @@ export function parseArgs(argv: readonly string[]): CliOptions { } case "--repeats": { const n = Number(next()); - if (!Number.isInteger(n) || n <= 0) throw new Error("--repeats must be a positive integer"); + if (!Number.isInteger(n) || n <= 0) + throw new Error("--repeats must be a positive integer"); opts.repeats = n; break; } @@ -305,7 +326,12 @@ function runCommand( cwd: string, timeoutMs: number, extraEnv: Record = {}, -): Promise<{ exitCode: number; stdout: string; stderr: string; timedOut: boolean }> { +): Promise<{ + exitCode: number; + stdout: string; + stderr: string; + timedOut: boolean; +}> { return new Promise((resolvePromise) => { const child = spawn(command, [...args], { cwd, @@ -352,7 +378,11 @@ function runCommand( }); } -async function withTimeout(promise: Promise, timeoutMs: number, label: string): Promise { +async function withTimeout( + promise: Promise, + timeoutMs: number, + label: string, +): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ @@ -410,7 +440,12 @@ async function seedEvalSkillStubs(workdir: string): Promise { * key. Do not call this on source fixtures. */ export async function initEvalGitRepo(workdir: string): Promise { - const identity = ["-c", "user.email=eval@local", "-c", "user.name=eval"] as const; + const identity = [ + "-c", + "user.email=eval@local", + "-c", + "user.name=eval", + ] as const; const git = async (args: readonly string[]): Promise => { const result = await runCommand("git", args, workdir, 30_000); if (result.exitCode !== 0) { @@ -454,7 +489,10 @@ async function prepareWorkdir( * the full turn stream (tool calls + assistant content) for behavior metrics. * The hook writes the postRun payload verbatim and swallows other kinds. */ -async function installRunCaptureHook(workdir: string, capturePath: string): Promise { +async function installRunCaptureHook( + workdir: string, + capturePath: string, +): Promise { const hooksDir = join(workdir, SETTINGS_DIR_NAME, "hooks"); await mkdir(hooksDir, { recursive: true }); const script = [ @@ -471,7 +509,9 @@ async function installRunCaptureHook(workdir: string, capturePath: string): Prom await chmod(hookPath, 0o755); } -async function readCapturedBehaviors(capturePath: string): Promise { +async function readCapturedBehaviors( + capturePath: string, +): Promise { try { const raw: unknown = JSON.parse(await readFile(capturePath, "utf8")); return deriveBehaviorMetrics(parseCapturedRunSummary(raw)); @@ -526,11 +566,22 @@ async function runVerify( workdir: string, timeoutMs: number, extraEnv: Record = {}, -): Promise<{ exitCode: number; output: string; durationMs: number; timedOut: boolean }> { +): Promise<{ + exitCode: number; + output: string; + durationMs: number; + timedOut: boolean; +}> { const verifyPath = join(caseDef.caseDir, caseDef.verify); await chmod(verifyPath, 0o755).catch(() => undefined); const started = Date.now(); - const result = await runCommand("bash", [verifyPath], workdir, timeoutMs, extraEnv); + const result = await runCommand( + "bash", + [verifyPath], + workdir, + timeoutMs, + extraEnv, + ); const output = [result.stdout, result.stderr].filter(Boolean).join("\n"); return { exitCode: result.exitCode, @@ -546,7 +597,8 @@ async function resolveVariantLabels( ): Promise<{ provider: string; model: string }> { try { const probe: string[] = ["exec", "--cwd", REPO_ROOT]; - if (variant.provider !== undefined) probe.push("--provider", variant.provider); + if (variant.provider !== undefined) + probe.push("--provider", variant.provider); if (variant.model !== undefined) probe.push("--model", variant.model); if (opts.configPath !== undefined) probe.push("--config", opts.configPath); probe.push("--force", "probe"); @@ -618,9 +670,14 @@ async function applyEvalEffort( * reasoningEffort echoes the configured value, not the provider's internal * default when unset — accepted as-is per review. */ -export async function buildEvalDiagnostics(config: Config): Promise { - const localSettings = await loadLocalSettings(localSettingsPath(config.cwd)).catch(() => null); - const sessionMode = resolveSessionMode(config.settings, localSettings) ?? "orchestrator"; +export async function buildEvalDiagnostics( + config: Config, +): Promise { + const localSettings = await loadLocalSettings( + localSettingsPath(config.cwd), + ).catch(() => null); + const sessionMode = + resolveSessionMode(config.settings, localSettings) ?? "orchestrator"; const overlay = resolveExecDirectorOverlay(config.director); const advertisedTools = overlay.advertisedAllow ?? @@ -715,7 +772,8 @@ async function runCase( // exists to catch, not commit. const requested = resolveRequestedProviderModel(variant, labels); const argv: string[] = ["exec", "--cwd", workdir]; - if (requested.provider !== undefined) argv.push("--provider", requested.provider); + if (requested.provider !== undefined) + argv.push("--provider", requested.provider); if (requested.model !== undefined) argv.push("--model", requested.model); if (opts.configPath !== undefined) argv.push("--config", opts.configPath); if (opts.skipPermissions) argv.push("--dangerously-skip-permissions"); @@ -761,18 +819,25 @@ async function runCase( console.log(`agent error: ${execResult.error}`); } - const resolvedProvider = execResult.provider ?? config.providerName ?? labels.provider; + const resolvedProvider = + execResult.provider ?? config.providerName ?? labels.provider; const resolvedModel = execResult.model ?? config.model ?? labels.model; providerFallback = detectProviderFallback({ - ...(requested.provider !== undefined ? { requestedProvider: requested.provider } : {}), - ...(requested.model !== undefined ? { requestedModel: requested.model } : {}), + ...(requested.provider !== undefined + ? { requestedProvider: requested.provider } + : {}), + ...(requested.model !== undefined + ? { requestedModel: requested.model } + : {}), resolvedProvider, resolvedModel, }); if (providerFallback !== null) { const message = formatProviderFallback(providerFallback); if (!opts.allowProviderFallback) { - throw new Error(`${message} (pass --allow-provider-fallback to allow this)`); + throw new Error( + `${message} (pass --allow-provider-fallback to allow this)`, + ); } console.warn(`[eval] ${message}`); } @@ -791,7 +856,8 @@ async function runCase( } const requireBehaviorCheck = - caseDef.requireBehaviors !== undefined && caseDef.requireBehaviors.length > 0 + caseDef.requireBehaviors !== undefined && + caseDef.requireBehaviors.length > 0 ? checkBehaviorRequirements(behaviors, caseDef.requireBehaviors) : { ok: true, failures: [] as string[] }; if (!requireBehaviorCheck.ok) { @@ -802,16 +868,24 @@ async function runCase( const verifyEnv: Record = httpFixture !== null ? httpFixtureEnv(httpFixture) : {}; - const verify = await runVerify(caseDef, workdir, opts.verifyTimeoutMs, verifyEnv); + const verify = await runVerify( + caseDef, + workdir, + opts.verifyTimeoutMs, + verifyEnv, + ); if (verify.output.trim().length > 0) { console.log(verify.output.trimEnd()); } console.log(`verify exit: ${verify.exitCode} (${verify.durationMs}ms)`); // requireBehaviors can fail a green agent+verify run (e.g. web-bait honesty). - const passed = agentExitCode === 0 && verify.exitCode === 0 && requireBehaviorCheck.ok; + const passed = + agentExitCode === 0 && verify.exitCode === 0 && requireBehaviorCheck.ok; const preview = - execResult.text.length > 400 ? `${execResult.text.slice(0, 400)}…` : execResult.text; + execResult.text.length > 400 + ? `${execResult.text.slice(0, 400)}…` + : execResult.text; let error: string | null = null; if (!passed) { @@ -832,7 +906,8 @@ async function runCase( let textPreview = preview; if (!requireBehaviorCheck.ok) { const reqNote = `requireBehaviors: ${requireBehaviorCheck.failures.join("; ")}`; - textPreview = textPreview.length > 0 ? `${reqNote}\n${textPreview}` : reqNote; + textPreview = + textPreview.length > 0 ? `${reqNote}\n${textPreview}` : reqNote; } return { @@ -866,15 +941,26 @@ async function runCase( } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error(`case ${caseDef.id} (${variant.id}) failed: ${message}`); - return failResult(caseDef, variant, labels, opts, started, repeat, message, { - providerFallback, - }); + return failResult( + caseDef, + variant, + labels, + opts, + started, + repeat, + message, + { + providerFallback, + }, + ); } finally { if (httpFixture !== null) { await httpFixture.close().catch(() => undefined); } if (workdir !== null) { - await rm(workdir, { recursive: true, force: true }).catch(() => undefined); + await rm(workdir, { recursive: true, force: true }).catch( + () => undefined, + ); } if (capturePath !== null) { await rm(capturePath, { force: true }).catch(() => undefined); @@ -924,25 +1010,33 @@ async function main(): Promise { if (opts.dryRun) { console.log("dry-run: no inference"); for (const { caseDef, variant } of plan) { - console.log(` would run: ${variant.id} × ${caseDef.id} × ${opts.repeats} repeat(s)`); + console.log( + ` would run: ${variant.id} × ${caseDef.id} × ${opts.repeats} repeat(s)`, + ); } return 0; } const startedAt = new Date().toISOString(); - const cells: { caseDef: EvalCase; variant: EvalVariant; repeat: number }[] = []; + const cells: { caseDef: EvalCase; variant: EvalVariant; repeat: number }[] = + []; for (const { caseDef, variant } of plan) { for (let repeat = 0; repeat < opts.repeats; repeat++) { cells.push({ caseDef, variant, repeat }); } } - const results = await mapPool(cells, opts.concurrency, ({ caseDef, variant, repeat }) => - runCase(caseDef, variant, opts, repeat), + const results = await mapPool( + cells, + opts.concurrency, + ({ caseDef, variant, repeat }) => runCase(caseDef, variant, opts, repeat), ); const finishedAt = new Date().toISOString(); const totals = summarizeRun(results); - const primary = variants[0]!; + const primary = variants[0]; + if (primary === undefined) { + throw new Error("matrix produced no variants"); + } const labels = await resolveVariantLabels(primary, opts); const aggregates = computeCellAggregates(results); @@ -995,19 +1089,23 @@ async function main(): Promise { let exitCode = totals.failed > 0 ? 1 : 0; if (opts.baselinePath !== undefined) { - const raw: unknown = JSON.parse(await readFile(resolve(opts.baselinePath), "utf8")); + const raw: unknown = JSON.parse( + await readFile(resolve(opts.baselinePath), "utf8"), + ); const baseline = parseEvalRunReport(raw); const cmp = compareToBaseline(results, baseline, selected, { allowProviderFallback: opts.allowProviderFallback, }); console.log("\n=== Baseline compare (aggregates)"); for (const d of cmp.deltas) { - const rate = (r: number | null): string => (r === null ? "n/a" : r.toFixed(2)); + const rate = (r: number | null): string => + r === null ? "n/a" : r.toFixed(2); console.log( ` ${d.status.padEnd(10)} ${d.resultKey} passRate ${rate(d.previousPassRate)} -> ${rate(d.currentPassRate)}`, ); for (const v of d.behaviorVerdicts) { - if (v.verdict === "neutral" && v.baselineMedian === v.currentMedian) continue; + if (v.verdict === "neutral" && v.baselineMedian === v.currentMedian) + continue; console.log( ` ${v.verdict.padEnd(8)} ${v.metric} median ${v.baselineMedian} -> ${v.currentMedian}`, ); diff --git a/scripts/eval-public-swe-one.test.ts b/scripts/eval-public-swe-one.test.ts index 5335e5f3b..7167d65f5 100644 --- a/scripts/eval-public-swe-one.test.ts +++ b/scripts/eval-public-swe-one.test.ts @@ -16,7 +16,13 @@ describe("parseArgs", () => { }); test("--dry-run with provider and model parses", () => { - const opts = parseArgs(["--dry-run", "--provider", "foo", "--model", "bar"]); + const opts = parseArgs([ + "--dry-run", + "--provider", + "foo", + "--model", + "bar", + ]); expect(opts.dryRun).toBe(true); expect(opts.provider).toBe("foo"); expect(opts.model).toBe("bar"); diff --git a/scripts/eval-public-swe-one.ts b/scripts/eval-public-swe-one.ts index 1e92eff9f..d08bff741 100644 --- a/scripts/eval-public-swe-one.ts +++ b/scripts/eval-public-swe-one.ts @@ -88,7 +88,8 @@ export function parseArgs(argv: string[]): CliOptions { help: false, }; for (let i = 0; i < argv.length; i++) { - const a = argv[i]!; + const a = argv[i]; + if (a === undefined) continue; const next = () => { const v = argv[++i]; if (v === undefined) throw new Error(`missing value for ${a}`); @@ -170,7 +171,11 @@ function run( opts.timeoutMs !== undefined ? setTimeout(() => { child.kill("SIGKILL"); - reject(new Error(`timeout after ${opts.timeoutMs}ms: ${cmd} ${args.join(" ")}`)); + reject( + new Error( + `timeout after ${opts.timeoutMs}ms: ${cmd} ${args.join(" ")}`, + ), + ); }, opts.timeoutMs) : null; child.on("error", (err) => { @@ -213,16 +218,25 @@ for k in keys: out[k] = v if isinstance(v, str) else json.dumps(v) print(json.dumps(out)) `; - const result = await run("uv", ["run", "--with", "datasets", "python", "-c", py], { - timeoutMs: 180_000, - }); + const result = await run( + "uv", + ["run", "--with", "datasets", "python", "-c", py], + { + timeoutMs: 180_000, + }, + ); if (result.code !== 0) { - throw new Error(`failed to load instance:\n${result.stderr || result.stdout}`); + throw new Error( + `failed to load instance:\n${result.stderr || result.stdout}`, + ); } return JSON.parse(result.stdout.trim()) as SweInstance; } -async function prepareRepo(instance: SweInstance, workRoot: string): Promise { +async function prepareRepo( + instance: SweInstance, + workRoot: string, +): Promise { const repoDir = join(workRoot, "repo"); const url = `https://github.com/${instance.repo}.git`; console.log(`cloning ${url} …`); @@ -240,16 +254,26 @@ async function prepareRepo(instance: SweInstance, workRoot: string): Promise { +async function capturePatch( + repoDir: string, + baseCommit: string, +): Promise { // Stage everything, then diff the index tree against the SWE base commit so // we include new files and agent commits without depending on HEAD movement. await run("git", ["add", "-A"], { cwd: repoDir }); @@ -289,20 +316,31 @@ async function capturePatch(repoDir: string, baseCommit: string): Promise(p: Promise, ms: number, label: string): Promise { +async function withTimeout( + p: Promise, + ms: number, + label: string, +): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ p, new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); + timer = setTimeout( + () => reject(new Error(`${label} timed out after ${ms}ms`)), + ms, + ); }), ]); } finally { @@ -331,7 +369,10 @@ async function main(): Promise { console.log(`out: ${outDir}`); const instance = await loadInstance(opts); - await writeFile(join(outDir, "instance.json"), JSON.stringify(instance, null, 2)); + await writeFile( + join(outDir, "instance.json"), + JSON.stringify(instance, null, 2), + ); console.log( `loaded ${instance.instance_id} (${instance.repo} @ ${instance.base_commit.slice(0, 12)})`, ); @@ -436,10 +477,15 @@ async function main(): Promise { "Patch captured from host-side Corbits run. Official resolved/not-resolved " + "requires SWE-bench Docker eval (--evaluate or external harness).", }; - await writeFile(join(outDir, "report.json"), JSON.stringify(report, null, 2)); + await writeFile( + join(outDir, "report.json"), + JSON.stringify(report, null, 2), + ); // Keep a copy of the final tree for debugging (may be large — skip if huge). - console.log(`patch bytes: ${report.patchBytes}${report.patchEmpty ? " (EMPTY)" : ""}`); + console.log( + `patch bytes: ${report.patchBytes}${report.patchEmpty ? " (EMPTY)" : ""}`, + ); console.log(`report: ${join(outDir, "report.json")}`); if (opts.evaluate) { diff --git a/scripts/generate-homebrew-tap.ts b/scripts/generate-homebrew-tap.ts index beda3cb2e..9c660565b 100644 --- a/scripts/generate-homebrew-tap.ts +++ b/scripts/generate-homebrew-tap.ts @@ -67,12 +67,15 @@ end `; } -async function readFormulaRenames(path: string): Promise> { +async function readFormulaRenames( + path: string, +): Promise> { let raw: string; try { raw = await readFile(path, "utf8"); } catch (cause) { - if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return {}; + if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") + return {}; throw cause; } @@ -105,7 +108,10 @@ export async function generateHomebrewTap( await writeFile(renamesPath, renameMetadata); } -function parseRelease(args: string[]): { tapDir: string; release: HomebrewRelease } { +function parseRelease(args: string[]): { + tapDir: string; + release: HomebrewRelease; +} { if (args.length !== 6) { throw new Error( "usage: generate-homebrew-tap.ts TAP_DIR VERSION MACOS_ARM64 MACOS_X64 LINUX_ARM64 LINUX_X64", diff --git a/scripts/guard-real-projects-dir.ts b/scripts/guard-real-projects-dir.ts index 177bfcdce..5cc763363 100644 --- a/scripts/guard-real-projects-dir.ts +++ b/scripts/guard-real-projects-dir.ts @@ -54,7 +54,10 @@ async function main(): Promise { // arguments the full default suite runs via `bun run test`, so `bun run // check` behavior is unchanged. const shardArgs = process.argv.slice(2); - const testCommand = shardArgs.length > 0 ? ["run", "test:paths", ...shardArgs] : ["run", "test"]; + const testCommand = + shardArgs.length > 0 + ? ["run", "test:paths", ...shardArgs] + : ["run", "test"]; const child = spawn("bun", testCommand, { stdio: "inherit", @@ -64,7 +67,7 @@ async function main(): Promise { child.on("exit", (code) => resolve(code ?? 1)); }); - await rm(runTmpDir, { recursive: true, force: true }).catch(() => {}); + await rm(runTmpDir, { recursive: true, force: true }).catch(() => undefined); const after = await listEntries(); const newEntries = [...after].filter((name) => !before.has(name)); diff --git a/scripts/intervention-forensics.ts b/scripts/intervention-forensics.ts index 6418b0941..314da1617 100644 --- a/scripts/intervention-forensics.ts +++ b/scripts/intervention-forensics.ts @@ -33,7 +33,10 @@ import { readdirSync, lstatSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { INTERVENTION_FILE, type InterventionRecord } from "../src/subagent/intervention-log.js"; +import { + INTERVENTION_FILE, + type InterventionRecord, +} from "../src/subagent/intervention-log.js"; // lstat, and skip symlinks: session dirs carry a `latest` symlink to a real // session, and following it double-counts every record in that session. @@ -60,8 +63,13 @@ function findAll(dir: string, name: string, out: string[]): void { function percentile(sorted: readonly number[], p: number): number { if (sorted.length === 0) return 0; - const index = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length)); - return sorted[index]!; + const index = Math.min( + sorted.length - 1, + Math.floor((p / 100) * sorted.length), + ); + const value = sorted[index]; + if (value === undefined) return 0; + return value; } interface Bucket { @@ -127,7 +135,10 @@ for (const file of files) { const kind = record.outcome.kind; outcomes.set(kind, (outcomes.get(kind) ?? 0) + 1); if (record.model !== undefined) { - dispatchesByModel.set(record.model, (dispatchesByModel.get(record.model) ?? 0) + 1); + dispatchesByModel.set( + record.model, + (dispatchesByModel.get(record.model) ?? 0) + 1, + ); } else { // Written before CL-6968 tagged outcome records with model identity. untaggedOutcomes++; @@ -146,7 +157,10 @@ for (const file of files) { const model = record.model ?? "unknown"; bucket.byModel.set(model, (bucket.byModel.get(model) ?? 0) + 1); if (record.class === "stop" || record.class === "nudge") { - interventionsByModel.set(model, (interventionsByModel.get(model) ?? 0) + 1); + interventionsByModel.set( + model, + (interventionsByModel.get(model) ?? 0) + 1, + ); } if (record.measurement !== undefined) { bucket.values.push(record.measurement.value); @@ -162,21 +176,27 @@ for (const file of files) { } console.log(`intervention logs: ${files.length}`); -console.log(`records: ${records}${malformed > 0 ? ` (${malformed} malformed, skipped)` : ""}`); +console.log( + `records: ${records}${malformed > 0 ? ` (${malformed} malformed, skipped)` : ""}`, +); if (records === 0) { console.log("\nNo interventions logged yet. Run some sessions first."); process.exit(0); } const rows = [...buckets.entries()].sort((a, b) => b[1].count - a[1].count); -console.log("\nintervention n value p50/p90/max threshold edited"); +console.log( + "\nintervention n value p50/p90/max threshold edited", +); for (const [key, bucket] of rows) { const sorted = [...bucket.values].sort((a, b) => a - b); + const last = sorted[sorted.length - 1]; const dist = - sorted.length === 0 + sorted.length === 0 || last === undefined ? "-" - : `${percentile(sorted, 50)}/${percentile(sorted, 90)}/${sorted[sorted.length - 1]!}`; - const thresholds = bucket.thresholds.size === 0 ? "-" : [...bucket.thresholds].join(","); + : `${percentile(sorted, 50)}/${percentile(sorted, 90)}/${last}`; + const thresholds = + bucket.thresholds.size === 0 ? "-" : [...bucket.thresholds].join(","); console.log( `${key.padEnd(33)} ${String(bucket.count).padStart(3)} ${dist.padEnd(16)} ${thresholds.padEnd(10)} ${String(bucket.editedWork).padStart(5)}`, ); @@ -205,7 +225,9 @@ if (repetitionRows.length > 0) { totalsByModel.set(model, (totalsByModel.get(model) ?? 0) + count); } } - console.log("\nrepetition aborts by model (mid-stream degenerate-repetition, all detectors)"); + console.log( + "\nrepetition aborts by model (mid-stream degenerate-repetition, all detectors)", + ); const modelRows = [...totalsByModel.entries()].sort((a, b) => b[1] - a[1]); for (const [model, count] of modelRows) { console.log(`${model.padEnd(33)} ${count}`); @@ -228,13 +250,19 @@ console.log( // dispatches = outcome records tagged with that model (CL-6968); interventions // = stop+nudge records for that model. Everything above this is a count. if (dispatchesByModel.size > 0 || interventionsByModel.size > 0) { - console.log("\ninterventions per dispatch by model (stop+nudge count / dispatch count = rate)"); - const models = new Set([...dispatchesByModel.keys(), ...interventionsByModel.keys()]); + console.log( + "\ninterventions per dispatch by model (stop+nudge count / dispatch count = rate)", + ); + const models = new Set([ + ...dispatchesByModel.keys(), + ...interventionsByModel.keys(), + ]); const modelRows = [...models] .map((model) => { const dispatches = dispatchesByModel.get(model) ?? 0; const interventions = interventionsByModel.get(model) ?? 0; - const rate = dispatches > 0 ? (interventions / dispatches).toFixed(3) : "-"; + const rate = + dispatches > 0 ? (interventions / dispatches).toFixed(3) : "-"; return { model, dispatches, interventions, rate }; }) .sort((a, b) => b.interventions - a.interventions); diff --git a/scripts/oxlint-plugin-corbits.js b/scripts/oxlint-plugin-corbits.js new file mode 100644 index 000000000..bf9e0b273 --- /dev/null +++ b/scripts/oxlint-plugin-corbits.js @@ -0,0 +1,46 @@ +const noBareMockModule = { + meta: { + type: "problem", + docs: { + description: + "Disallow bare mock.module in test files; it leaks into later files in the same bun test process.", + }, + messages: { + noBare: + "Use withMockedModule/withMockedModuleDuring from tests/helpers/mock-module.ts instead of bare mock.module — an un-restored mock.module leaks into every test file that runs after this one.", + }, + }, + create(context) { + return { + CallExpression(node) { + const callee = node.callee; + if (callee.type !== "MemberExpression") return; + if (callee.computed) return; + if ( + callee.object.type !== "Identifier" || + callee.object.name !== "mock" + ) { + return; + } + if ( + callee.property.type !== "Identifier" || + callee.property.name !== "module" + ) { + return; + } + context.report({ node, messageId: "noBare" }); + }, + }; + }, +}; + +const plugin = { + meta: { + name: "corbits", + }, + rules: { + "no-bare-mock-module": noBareMockModule, + }, +}; + +export default plugin; diff --git a/scripts/test-paths.ts b/scripts/test-paths.ts index 4bb7c8ad9..f021245e9 100644 --- a/scripts/test-paths.ts +++ b/scripts/test-paths.ts @@ -14,9 +14,13 @@ if (paths.length === 0) { process.exit(1); } -const child = spawn("bun", ["test", "--randomize", "--seed", "424242", ...args], { - stdio: "inherit", -}); +const child = spawn( + "bun", + ["test", "--randomize", "--seed", "424242", ...args], + { + stdio: "inherit", + }, +); child.on("exit", (code) => { process.exit(code ?? 1); diff --git a/scripts/tool-fingerprint-forensics.ts b/scripts/tool-fingerprint-forensics.ts index a6a81bfca..1c35e6e5f 100644 --- a/scripts/tool-fingerprint-forensics.ts +++ b/scripts/tool-fingerprint-forensics.ts @@ -25,7 +25,9 @@ function stableJson(value: unknown): string { return `{${keys.map((k) => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(",")}}`; } -function fingerprintToolCalls(content: readonly Record[]): string | null { +function fingerprintToolCalls( + content: readonly Record[], +): string | null { const parts: string[] = []; for (const block of content) { if (block.type !== "tool_call") continue; @@ -112,9 +114,12 @@ for (const file of files) { const content = turn.content as readonly Record[]; const hasToolCalls = content.some((b) => b.type === "tool_call"); const hasText = content.some( - (b) => b.type === "text" && typeof b.text === "string" && b.text.length > 0, + (b) => + b.type === "text" && typeof b.text === "string" && b.text.length > 0, + ); + fingerprints.push( + hasToolCalls && !hasText ? fingerprintToolCalls(content) : null, ); - fingerprints.push(hasToolCalls && !hasText ? fingerprintToolCalls(content) : null); } const runs: string[][] = []; @@ -146,7 +151,10 @@ for (const file of files) { runLengths.sort((a, b) => a - b); function percentile(p: number): number { if (runLengths.length === 0) return 0; - const idx = Math.min(runLengths.length - 1, Math.floor((p / 100) * runLengths.length)); + const idx = Math.min( + runLengths.length - 1, + Math.floor((p / 100) * runLengths.length), + ); return runLengths[idx] as number; } diff --git a/src/agent/agent-search.test.ts b/src/agent/agent-search.test.ts index b24331746..2782cc283 100644 --- a/src/agent/agent-search.test.ts +++ b/src/agent/agent-search.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { CREDENTIAL_REDACTION } from "../plugins/tool-result-secret-scrub.js"; import { @@ -11,11 +12,13 @@ import type { AgentProfile } from "./profiles.js"; const fixtures: AgentProfile[] = [ { id: "greybeard", - description: "Seasoned architect — reviews for design and backwards compatibility", + description: + "Seasoned architect — reviews for design and backwards compatibility", }, { id: "critique", - description: "Code quality reviewer — tests assumptions and security smells", + description: + "Code quality reviewer — tests assumptions and security smells", }, { id: "scout", @@ -42,7 +45,7 @@ describe("createAgentIndex", () => { describe("formatAgentSearchResults", () => { test("includes spawn hint and ids", () => { - const text = formatAgentSearchResults([fixtures[1]!]); + const text = formatAgentSearchResults([defined(fixtures[1])]); expect(text).toContain("critique"); expect(text).toContain("spawn_agent(agent="); }); @@ -79,7 +82,9 @@ describe("formatAgentSearchResults", () => { }); test("omits body section when systemPromptRole is absent", () => { - const text = formatAgentSearchResults([{ id: "no-body", description: "Metadata only" }]); + const text = formatAgentSearchResults([ + { id: "no-body", description: "Metadata only" }, + ]); expect(text).toContain("### no-body"); expect(text).toContain("Metadata only"); expect(text).not.toContain("System prompt / body:"); @@ -100,7 +105,9 @@ describe("formatAgentSearchResults", () => { const bodySection = text.split("System prompt / body:\n")[1] ?? ""; const injected = bodySection.split("\n\nSpawn with")[0] ?? bodySection; expect(injected.length).toBeLessThan(body.length); - expect(injected.startsWith("x".repeat(MAX_AGENT_SEARCH_BODY_CHARS))).toBe(true); + expect(injected.startsWith("x".repeat(MAX_AGENT_SEARCH_BODY_CHARS))).toBe( + true, + ); }); test("redacts secret-shaped content in profile body at format layer", () => { @@ -139,7 +146,10 @@ describe("createSearchAgentsTool", () => { }, ]); if (tool.kind !== "string") throw new Error("expected string tool"); - const text = await tool.handler({ query: "emil product" }, new AbortController().signal); + const text = await tool.handler( + { query: "emil product" }, + new AbortController().signal, + ); expect(text).toContain("emil"); expect(text).toContain("System prompt / body:"); expect(text).toContain(body); @@ -154,7 +164,10 @@ describe("createSearchAgentsTool", () => { })); const tool = createSearchAgentsTool(() => many); if (tool.kind !== "string") throw new Error("expected string tool"); - const text = await tool.handler({ query: "" }, new AbortController().signal); + const text = await tool.handler( + { query: "" }, + new AbortController().signal, + ); const headers = [...text.matchAll(/^### (agent-\d+)/gm)].map((m) => m[1]); expect(headers).toHaveLength(12); expect(headers[0]).toBe("agent-00"); @@ -165,7 +178,10 @@ describe("createSearchAgentsTool", () => { test("empty catalog + empty query returns loaded-none message", async () => { const tool = createSearchAgentsTool(() => []); if (tool.kind !== "string") throw new Error("expected string tool"); - const text = await tool.handler({ query: " " }, new AbortController().signal); + const text = await tool.handler( + { query: " " }, + new AbortController().signal, + ); expect(text).toBe("No agent profiles are loaded."); }); @@ -182,7 +198,10 @@ describe("createSearchAgentsTool", () => { }, ]); if (tool.kind !== "string") throw new Error("expected string tool"); - const text = await tool.handler({ query: "leaky" }, new AbortController().signal); + const text = await tool.handler( + { query: "leaky" }, + new AbortController().signal, + ); expect(text).toContain("### leaky"); expect(text).toContain(CREDENTIAL_REDACTION); expect(text).not.toContain(secret); diff --git a/src/agent/agent-search.ts b/src/agent/agent-search.ts index 4a1c6ca83..ba9168c02 100644 --- a/src/agent/agent-search.ts +++ b/src/agent/agent-search.ts @@ -10,7 +10,11 @@ function tokenize(text: string): string[] { } function profileSearchText(profile: AgentProfile): string { - const parts = [profile.id, profile.description ?? "", profile.systemPromptRole ?? ""]; + const parts = [ + profile.id, + profile.description ?? "", + profile.systemPromptRole ?? "", + ]; return parts.join(" "); } @@ -19,8 +23,14 @@ export interface AgentIndex { } // Lexical ranker over id, description, and role text — same spirit as tool_search. -export function createAgentIndex(getProfiles: () => readonly AgentProfile[]): AgentIndex { - const score = (profile: AgentProfile, queryTokens: string[], rawQuery: string): number => { +export function createAgentIndex( + getProfiles: () => readonly AgentProfile[], +): AgentIndex { + const score = ( + profile: AgentProfile, + queryTokens: string[], + rawQuery: string, + ): number => { const idTokens = tokenize(profile.id); const blob = profileSearchText(profile).toLowerCase(); const blobTokens = new Set(tokenize(blob)); @@ -32,7 +42,8 @@ export function createAgentIndex(getProfiles: () => readonly AgentProfile[]): Ag else if (blob.includes(token)) total += 0.25; } if (profile.id.toLowerCase().includes(rawQuery)) total += 1; - if ((profile.description ?? "").toLowerCase().includes(rawQuery)) total += 0.5; + if ((profile.description ?? "").toLowerCase().includes(rawQuery)) + total += 0.5; return total; }; @@ -71,13 +82,17 @@ function formatAgentProfileEntry(p: AgentProfile): string { const orch = p.orchestrator === true ? " [orchestrator]" : ""; const source = p.source !== undefined ? ` [source: ${p.source}]` : ""; const header = - desc.length > 0 ? `### ${p.id}${orch}${source}\n${desc}` : `### ${p.id}${orch}${source}`; + desc.length > 0 + ? `### ${p.id}${orch}${source}\n${desc}` + : `### ${p.id}${orch}${source}`; const body = (p.systemPromptRole ?? "").trim(); if (body.length === 0) return header; return `${header}\n\nSystem prompt / body:\n${truncateAgentBody(body)}`; } -export function formatAgentSearchResults(profiles: readonly AgentProfile[]): string { +export function formatAgentSearchResults( + profiles: readonly AgentProfile[], +): string { if (profiles.length === 0) { return "No agent profiles matched. Try broader terms (e.g. review, explore, implement) or list_dir on .agents/agents/."; } @@ -116,7 +131,9 @@ export const searchAgentsDefinition: ToolDefinition = { const SearchAgentsArgs = type({ query: "string" }); -export function createSearchAgentsTool(getProfiles: () => readonly AgentProfile[]): AgentTool { +export function createSearchAgentsTool( + getProfiles: () => readonly AgentProfile[], +): AgentTool { const index = createAgentIndex(getProfiles); return stringTool({ definition: searchAgentsDefinition, diff --git a/src/agent/apply-patch-diff.test.ts b/src/agent/apply-patch-diff.test.ts index d1da5cd55..162b93304 100644 --- a/src/agent/apply-patch-diff.test.ts +++ b/src/agent/apply-patch-diff.test.ts @@ -6,7 +6,10 @@ import { createPosixTools } from "@intx/tools-posix"; import { createToolRunner } from "@intx/agent"; import type { AgentTool } from "@intx/agent"; -import { createCodexToolProxies, type CodexRunTool } from "./codex-tool-proxies.js"; +import { + createCodexToolProxies, + type CodexRunTool, +} from "./codex-tool-proxies.js"; import { createCodexReadRawFile } from "./codex-read-raw-file.js"; import { buildCorePosixToolPlugins } from "./posix-tool-plugins.js"; import { createPermissionGate } from "../permission/gate.js"; @@ -46,7 +49,10 @@ async function makeApplyPatch( new AbortController().signal, ); return { - content: typeof result.content === "string" ? result.content : JSON.stringify(result.content), + content: + typeof result.content === "string" + ? result.content + : JSON.stringify(result.content), ...(result.isError === true ? { isError: true } : {}), }; }; @@ -124,7 +130,11 @@ describe("apply_patch Update File matches raw content, not read_file's numbered try { await writeFile(join(cwd, "gone.txt"), "bye\n"); const tools = await makeApplyPatch(cwd); - const input = ["*** Begin Patch", "*** Delete File: gone.txt", "*** End Patch"].join("\n"); + const input = [ + "*** Begin Patch", + "*** Delete File: gone.txt", + "*** End Patch", + ].join("\n"); const result = await invokeApplyPatch(tools, input); @@ -139,9 +149,12 @@ describe("apply_patch Update File matches raw content, not read_file's numbered const cwd = await mkdtemp(join(tmpdir(), "apply-patch-diff-")); try { const tools = await makeApplyPatch(cwd); - const input = ["*** Begin Patch", "*** Add File: new.txt", "+hello", "*** End Patch"].join( - "\n", - ); + const input = [ + "*** Begin Patch", + "*** Add File: new.txt", + "+hello", + "*** End Patch", + ].join("\n"); const result = await invokeApplyPatch(tools, input); @@ -218,7 +231,10 @@ describe("apply_patch Update File refuses reads outside the sanctioned workspace // skipPermissions: true (yolo) — secret-guard has no bypass, unlike containment. const tools = await makeApplyPatch(cwd, { skipPermissions: true }); - const result = await invokeApplyPatch(tools, insertionOnlyMoveInput(".env", "leaked.txt")); + const result = await invokeApplyPatch( + tools, + insertionOnlyMoveInput(".env", "leaked.txt"), + ); expect(result.isError).toBe(true); expect(String(result.content)).toMatch(/sensitive file/i); @@ -230,17 +246,24 @@ describe("apply_patch Update File refuses reads outside the sanctioned workspace }); test("../ traversal to a secret file is refused (secret-guard applies to relative paths too)", async () => { - const parent = await mkdtemp(join(tmpdir(), "apply-patch-cl6966-secret-parent-")); + const parent = await mkdtemp( + join(tmpdir(), "apply-patch-cl6966-secret-parent-"), + ); const cwd = join(parent, "workspace"); await mkdir(cwd); try { await writeFile(join(parent, ".env"), "API_KEY=super-secret\n"); const tools = await makeApplyPatch(cwd, { skipPermissions: false }); - const result = await invokeApplyPatch(tools, insertionOnlyMoveInput("../.env", "leaked.txt")); + const result = await invokeApplyPatch( + tools, + insertionOnlyMoveInput("../.env", "leaked.txt"), + ); expect(result.isError).toBe(true); - expect(String(result.content)).toMatch(/sensitive file|escapes working directory/i); + expect(String(result.content)).toMatch( + /sensitive file|escapes working directory/i, + ); expect(await Bun.file(join(cwd, "leaked.txt")).exists()).toBe(false); } finally { await rm(parent, { recursive: true, force: true }); diff --git a/src/agent/background-shell-tool.test.ts b/src/agent/background-shell-tool.test.ts index 33125b151..025a52b46 100644 --- a/src/agent/background-shell-tool.test.ts +++ b/src/agent/background-shell-tool.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; @@ -36,7 +37,9 @@ describe("background shell through the agent toolset", () => { new AbortController().signal, ); expect(started.isError).not.toBe(true); - const parsed = JSON.parse(String(started.content)) as { shell_id: string }; + const parsed = JSON.parse(String(started.content)) as { + shell_id: string; + }; const snapshotNow = await toolset.dynamicRunner.run( { id: "bg-collect", @@ -45,12 +48,18 @@ describe("background shell through the agent toolset", () => { }, new AbortController().signal, ); - expect(JSON.parse(String(snapshotNow.content))).toMatchObject({ status: "running" }); + expect(JSON.parse(String(snapshotNow.content))).toMatchObject({ + status: "running", + }); const final = await toolset.dynamicRunner.run( { id: "bg-collect2", name: "shell_collect", - arguments: { shell_id: parsed.shell_id, action: "collect", wait_ms: 5_000 }, + arguments: { + shell_id: parsed.shell_id, + action: "collect", + wait_ms: 5_000, + }, }, new AbortController().signal, ); @@ -63,9 +72,11 @@ describe("background shell through the agent toolset", () => { expect(result.output).toContain("bg-done"); await new Promise((r) => setTimeout(r, 50)); expect(exits).toHaveLength(1); - expect(exits[0]!.id).toBe(parsed.shell_id); - const message = buildShellBackgroundMessage(exits[0]!); - expect(message.headers.messageId).toBe(`bg-shell-${parsed.shell_id}@local`); + expect(defined(exits[0]).id).toBe(parsed.shell_id); + const message = buildShellBackgroundMessage(defined(exits[0])); + expect(message.headers.messageId).toBe( + `bg-shell-${parsed.shell_id}@local`, + ); expect(message.ref.mailbox).toBe("system"); expect(message.flags).not.toContain(OPERATOR_ORIGINATED_FLAG); expect(message.content).toContain("exit code 0"); @@ -94,7 +105,9 @@ describe("background shell through the agent toolset", () => { }, new AbortController().signal, ); - const { shell_id } = JSON.parse(String(started.content)) as { shell_id: string }; + const { shell_id } = JSON.parse(String(started.content)) as { + shell_id: string; + }; const cancelled = await toolset.dynamicRunner.run( { id: "c-cancel", @@ -103,7 +116,9 @@ describe("background shell through the agent toolset", () => { }, new AbortController().signal, ); - expect(JSON.parse(String(cancelled.content))).toMatchObject({ status: "cancelling" }); + expect(JSON.parse(String(cancelled.content))).toMatchObject({ + status: "cancelling", + }); await new Promise((r) => setTimeout(r, 300)); const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" }); expect(probe.stdout?.trim() ?? "").toBe(""); diff --git a/src/agent/background-shell-tool.ts b/src/agent/background-shell-tool.ts index 459fbd831..ddd38b759 100644 --- a/src/agent/background-shell-tool.ts +++ b/src/agent/background-shell-tool.ts @@ -25,11 +25,15 @@ export const shellCollectDefinition: ToolDefinition = { inputSchema: { type: "object", properties: { - shell_id: { type: "string", description: "shell_id from the background run_shell start." }, + shell_id: { + type: "string", + description: "shell_id from the background run_shell start.", + }, action: { type: "string", enum: ["collect", "cancel"], - description: '"collect" retrieves status/output; "cancel" kills the process group.', + description: + '"collect" retrieves status/output; "cancel" kills the process group.', }, wait_ms: { type: "number", @@ -54,7 +58,11 @@ export function createSpillingBackgroundShellExitNotifier(args: { const writeBlob = args.getBlobWriter?.(); if (writeBlob !== undefined) { const key = `bg-shell-${exit.id}`; - await writeBlob(key, new TextEncoder().encode(exit.output), "text/plain"); + await writeBlob( + key, + new TextEncoder().encode(exit.output), + "text/plain", + ); spillUri = `tool-output:///${key}`; } } diff --git a/src/agent/codex-apply-patch.test.ts b/src/agent/codex-apply-patch.test.ts index 8ff0c5da6..b1af9325e 100644 --- a/src/agent/codex-apply-patch.test.ts +++ b/src/agent/codex-apply-patch.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { CodexApplyPatchError, @@ -28,7 +29,9 @@ describe("parseCodexApplyPatch", () => { *** Add File: empty.txt *** End Patch `); - expect(patch.ops).toEqual([{ type: "add", path: "empty.txt", content: "" }]); + expect(patch.ops).toEqual([ + { type: "add", path: "empty.txt", content: "" }, + ]); }); test("parses Delete File", () => { @@ -49,14 +52,14 @@ describe("parseCodexApplyPatch", () => { *** End Patch `); expect(patch.ops).toHaveLength(1); - const op = patch.ops[0]!; + const op = defined(patch.ops[0]); expect(op.type).toBe("update"); if (op.type !== "update") throw new Error("unreachable"); expect(op.path).toBe("src/app.py"); expect(op.moveTo).toBe("src/main.py"); expect(op.hunks).toHaveLength(1); - expect(op.hunks[0]!.header).toBe("def greet():"); - expect(op.hunks[0]!.lines).toEqual([ + expect(defined(op.hunks[0]).header).toBe("def greet():"); + expect(defined(op.hunks[0]).lines).toEqual([ { kind: "-", text: 'print("Hi")' }, { kind: "+", text: 'print("Hello, world!")' }, ]); @@ -71,13 +74,13 @@ describe("parseCodexApplyPatch", () => { +new_line *** End Patch `); - const op = patch.ops[0]!; + const op = defined(patch.ops[0]); expect(op.type).toBe("update"); if (op.type !== "update") throw new Error("unreachable"); expect(op.hunks).toHaveLength(2); expect(op.hunks[0]).toEqual({ header: "class BaseClass", lines: [] }); - expect(op.hunks[1]!.header).toBe(" def method():"); - expect(op.hunks[1]!.lines).toEqual([ + expect(defined(op.hunks[1]).header).toBe(" def method():"); + expect(defined(op.hunks[1]).lines).toEqual([ { kind: "-", text: "old_line" }, { kind: "+", text: "new_line" }, ]); @@ -157,7 +160,11 @@ describe("extractAffectedPaths", () => { *** Delete File: obsolete.txt *** End Patch `); - expect(extractAffectedPaths(patch)).toEqual(["hello.txt", "src/app.py", "obsolete.txt"]); + expect(extractAffectedPaths(patch)).toEqual([ + "hello.txt", + "src/app.py", + "obsolete.txt", + ]); }); test("move path extraction includes source and destination", () => { @@ -186,7 +193,7 @@ print("bye") +print("Hello, world!") *** End Patch `); - const op = patch.ops[0]!; + const op = defined(patch.ops[0]); expect(op.type).toBe("update"); if (op.type !== "update") throw new Error("unreachable"); const updated = applyUpdateHunks(original, op.hunks); @@ -210,7 +217,7 @@ print("bye") + new_line *** End Patch `); - const op = patch.ops[0]!; + const op = defined(patch.ops[0]); expect(op.type).toBe("update"); if (op.type !== "update") throw new Error("unreachable"); expect(applyUpdateHunks(original, op.hunks)).toBe(`class BaseClass diff --git a/src/agent/codex-apply-patch.ts b/src/agent/codex-apply-patch.ts index 817be6396..7cb98e9b0 100644 --- a/src/agent/codex-apply-patch.ts +++ b/src/agent/codex-apply-patch.ts @@ -89,10 +89,14 @@ export function parseCodexApplyPatch(input: string): ParsedPatch { } if (lines[0]?.trim() !== BEGIN_PATCH) { - throw new CodexApplyPatchError("malformed envelope: first line must be '*** Begin Patch'"); + throw new CodexApplyPatchError( + "malformed envelope: first line must be '*** Begin Patch'", + ); } if (lines[lines.length - 1]?.trim() !== END_PATCH) { - throw new CodexApplyPatchError("malformed envelope: last line must be '*** End Patch'"); + throw new CodexApplyPatchError( + "malformed envelope: last line must be '*** End Patch'", + ); } const body = lines.slice(1, -1); @@ -100,50 +104,72 @@ export function parseCodexApplyPatch(input: string): ParsedPatch { let i = 0; while (i < body.length) { - const line = body[i]!; + const line = body[i]; + if (line === undefined) { + throw new CodexApplyPatchError("unexpected end of patch body"); + } if (line.startsWith(ADD_FILE)) { const path = requireRelativePath(line.slice(ADD_FILE.length), "Add File"); i += 1; const contentLines: string[] = []; - while (i < body.length && body[i]!.startsWith("+")) { - contentLines.push(body[i]!.slice(1)); + while (i < body.length) { + const contentLine = body[i]; + if (contentLine === undefined || !contentLine.startsWith("+")) break; + contentLines.push(contentLine.slice(1)); i += 1; } - if (i < body.length && !isFileOpHeader(body[i]!)) { + const next = body[i]; + if (next !== undefined && !isFileOpHeader(next)) { throw new CodexApplyPatchError( - `malformed Add File '${path}': expected '+' content lines or next file op, got: ${body[i]}`, + `malformed Add File '${path}': expected '+' content lines or next file op, got: ${next}`, ); } // Codex-rs: each '+' line contributes text + "\n". - const content = contentLines.length === 0 ? "" : contentLines.map((l) => `${l}\n`).join(""); + const content = + contentLines.length === 0 + ? "" + : contentLines.map((l) => `${l}\n`).join(""); ops.push({ type: "add", path, content }); continue; } if (line.startsWith(DELETE_FILE)) { - const path = requireRelativePath(line.slice(DELETE_FILE.length), "Delete File"); + const path = requireRelativePath( + line.slice(DELETE_FILE.length), + "Delete File", + ); i += 1; ops.push({ type: "delete", path }); continue; } if (line.startsWith(UPDATE_FILE)) { - const path = requireRelativePath(line.slice(UPDATE_FILE.length), "Update File"); + const path = requireRelativePath( + line.slice(UPDATE_FILE.length), + "Update File", + ); i += 1; let moveTo: string | undefined; - if (i < body.length && body[i]!.startsWith(MOVE_TO)) { - moveTo = requireRelativePath(body[i]!.slice(MOVE_TO.length), "Move to"); + const maybeMove = body[i]; + if (maybeMove !== undefined && maybeMove.startsWith(MOVE_TO)) { + moveTo = requireRelativePath( + maybeMove.slice(MOVE_TO.length), + "Move to", + ); i += 1; } const hunks: PatchHunk[] = []; - while (i < body.length && isHunkStart(body[i]!)) { + while (i < body.length) { + const hunkLine = body[i]; + if (hunkLine === undefined || !isHunkStart(hunkLine)) break; const { hunk, next } = parseHunk(body, i); hunks.push(hunk); i = next; } - if (i < body.length && !isFileOpHeader(body[i]!)) { + const afterHunks = body[i]; + if (afterHunks !== undefined && !isFileOpHeader(afterHunks)) { throw new CodexApplyPatchError( - `malformed Update File '${path}': expected hunk ('@@') or next file op, got: ${body[i]}`, + `malformed Update File '${path}': expected hunk ('@@') or next file op, got: ${afterHunks}`, ); } ops.push( @@ -201,7 +227,9 @@ export function applyUpdateHunks(original: string, hunks: PatchHunk[]): string { if (hunk.header !== undefined && hunk.header.length > 0) { const idx = findLineFrom(lines, hunk.header, cursor); if (idx === -1) { - throw new CodexApplyPatchError(`failed to find hunk context header '${hunk.header}'`); + throw new CodexApplyPatchError( + `failed to find hunk context header '${hunk.header}'`, + ); } cursor = idx + 1; } @@ -223,13 +251,22 @@ export function applyUpdateHunks(original: string, hunks: PatchHunk[]): string { continue; } - const start = findSequence(lines, oldLines, cursor, hunk.endOfFile === true); + const start = findSequence( + lines, + oldLines, + cursor, + hunk.endOfFile === true, + ); if (start === -1) { throw new CodexApplyPatchError( `failed to find expected lines in file:\n${oldLines.join("\n")}`, ); } - lines = [...lines.slice(0, start), ...newLines, ...lines.slice(start + oldLines.length)]; + lines = [ + ...lines.slice(0, start), + ...newLines, + ...lines.slice(start + oldLines.length), + ]; cursor = start + newLines.length; } @@ -246,8 +283,14 @@ export function contentFromAddOp(op: PatchAddOp): string { return op.content; } -function parseHunk(body: string[], start: number): { hunk: PatchHunk; next: number } { - const headerLine = body[start]!; +function parseHunk( + body: string[], + start: number, +): { hunk: PatchHunk; next: number } { + const headerLine = body[start]; + if (headerLine === undefined) { + throw new CodexApplyPatchError("expected hunk start '@@'"); + } let header: string | undefined; if (headerLine === "@@") { header = undefined; @@ -256,18 +299,23 @@ function parseHunk(body: string[], start: number): { hunk: PatchHunk; next: numb } else if (headerLine.startsWith("@@")) { header = headerLine.slice(2).trimStart(); } else { - throw new CodexApplyPatchError(`expected hunk start '@@', got: ${headerLine}`); + throw new CodexApplyPatchError( + `expected hunk start '@@', got: ${headerLine}`, + ); } let i = start + 1; const lines: PatchHunkLine[] = []; while (i < body.length) { - const raw = body[i]!; + const raw = body[i]; + if (raw === undefined) break; if (raw === END_OF_FILE) { i += 1; return { hunk: - header === undefined ? { lines, endOfFile: true } : { header, lines, endOfFile: true }, + header === undefined + ? { lines, endOfFile: true } + : { header, lines, endOfFile: true }, next: i, }; } @@ -302,7 +350,11 @@ function parseHunk(body: string[], start: number): { hunk: PatchHunk; next: numb } function isFileOpHeader(line: string): boolean { - return line.startsWith(ADD_FILE) || line.startsWith(DELETE_FILE) || line.startsWith(UPDATE_FILE); + return ( + line.startsWith(ADD_FILE) || + line.startsWith(DELETE_FILE) || + line.startsWith(UPDATE_FILE) + ); } function isHunkStart(line: string): boolean { @@ -337,10 +389,12 @@ function findLineFrom(lines: string[], target: string, from: number): number { } // Soften header seek the same way as hunk body matching. for (let i = from; i < lines.length; i++) { - if (lines[i]!.trimEnd() === target.trimEnd()) return i; + const line = lines[i]; + if (line !== undefined && line.trimEnd() === target.trimEnd()) return i; } for (let i = from; i < lines.length; i++) { - if (lines[i]!.trim() === target.trim()) return i; + const line = lines[i]; + if (line !== undefined && line.trim() === target.trim()) return i; } return -1; } @@ -359,13 +413,20 @@ function findSequence( if (pattern.length > lines.length) return -1; const searchStart = - endOfFile && lines.length >= pattern.length ? lines.length - pattern.length : from; - - const tryFrom = (start: number, eq: (a: string, b: string) => boolean): number => { + endOfFile && lines.length >= pattern.length + ? lines.length - pattern.length + : from; + + const tryFrom = ( + start: number, + eq: (a: string, b: string) => boolean, + ): number => { for (let i = start; i <= lines.length - pattern.length; i++) { let ok = true; for (let j = 0; j < pattern.length; j++) { - if (!eq(lines[i + j]!, pattern[j]!)) { + const a = lines[i + j]; + const b = pattern[j]; + if (a === undefined || b === undefined || !eq(a, b)) { ok = false; break; } @@ -376,7 +437,8 @@ function findSequence( }; // When eof, try the eof-aligned window first, then fall through from `from`. - const starts = endOfFile && searchStart !== from ? [searchStart, from] : [searchStart]; + const starts = + endOfFile && searchStart !== from ? [searchStart, from] : [searchStart]; for (const start of starts) { const exact = tryFrom(start, (a, b) => a === b); diff --git a/src/agent/codex-read-raw-file.ts b/src/agent/codex-read-raw-file.ts index 01b03d6a6..b217d2a6e 100644 --- a/src/agent/codex-read-raw-file.ts +++ b/src/agent/codex-read-raw-file.ts @@ -26,7 +26,10 @@ import { resolve } from "node:path"; import { hasCode } from "@intx/types"; import { resolveWorkspacePath } from "../permission/path-restriction.js"; import { createWorktreeRootsProvider } from "../permission/worktree-roots.js"; -import { isSensitivePath, isSensitivePathResolved } from "../plugins/secret-guard-plugin.js"; +import { + isSensitivePath, + isSensitivePathResolved, +} from "../plugins/secret-guard-plugin.js"; import type { PermissionGate } from "../permission/gate.js"; import type { CodexReadRawFile } from "./codex-tool-proxies.js"; @@ -77,17 +80,25 @@ export function createCodexReadRawFile( try { const buf = await readFile(absolutePath); if (buf.includes(0)) { - return { content: `refusing to read binary file: ${path}`, isError: true }; + return { + content: `refusing to read binary file: ${path}`, + isError: true, + }; } return { content: buf.toString("utf8") }; } catch (err) { if (hasCode(err)) { - if (err.code === "ENOENT") return { content: `file not found: ${path}`, isError: true }; - if (err.code === "EACCES") return { content: `permission denied: ${path}`, isError: true }; + if (err.code === "ENOENT") + return { content: `file not found: ${path}`, isError: true }; + if (err.code === "EACCES") + return { content: `permission denied: ${path}`, isError: true }; if (err.code === "EISDIR") return { content: `path is a directory: ${path}`, isError: true }; } - return { content: err instanceof Error ? err.message : String(err), isError: true }; + return { + content: err instanceof Error ? err.message : String(err), + isError: true, + }; } }; } diff --git a/src/agent/codex-tool-mount.test.ts b/src/agent/codex-tool-mount.test.ts index 2c5e978c9..525fc0d7f 100644 --- a/src/agent/codex-tool-mount.test.ts +++ b/src/agent/codex-tool-mount.test.ts @@ -33,7 +33,7 @@ describe("Codex tool proxy mount", () => { spyOn(posixModule, "createPosixTools").mockReturnValue({ definitions: [], run: async () => ({ id: "x", content: "" }), - dispose: async () => {}, + dispose: async () => undefined, } as unknown as ReturnType); const { createAgentToolset } = await import("./tools.js"); @@ -133,15 +133,26 @@ describe("Codex tool proxy mount", () => { readRawFile: async () => ({ content: "ok" }), runManageTasks: async () => ({ content: "ok" }), }); - expect(proxies.map((t) => t.definition.name)).toEqual(["apply_patch", "shell", "update_plan"]); + expect(proxies.map((t) => t.definition.name)).toEqual([ + "apply_patch", + "shell", + "update_plan", + ]); const allow = new Set(BUILD_TOOLS); const kept = proxies.filter((t) => allow.has(t.definition.name)); - expect(kept.map((t) => t.definition.name)).toEqual(["apply_patch", "shell", "update_plan"]); + expect(kept.map((t) => t.definition.name)).toEqual([ + "apply_patch", + "shell", + "update_plan", + ]); const docsAllow = new Set(DOCS_TOOLS); const docsKept = proxies.filter((t) => docsAllow.has(t.definition.name)); - expect(docsKept.map((t) => t.definition.name)).toEqual(["apply_patch", "update_plan"]); + expect(docsKept.map((t) => t.definition.name)).toEqual([ + "apply_patch", + "update_plan", + ]); }); test("runSubAgent-shaped mount: docs capability filter denies shell, keeps update_plan", () => { @@ -158,11 +169,18 @@ describe("Codex tool proxy mount", () => { allowDelete: allowDeleteFromCapabilities(docsCapabilities), allowShell: allowShellFromCapabilities(docsCapabilities), }); - expect(proxies.map((t) => t.definition.name)).toEqual(["apply_patch", "shell", "update_plan"]); + expect(proxies.map((t) => t.definition.name)).toEqual([ + "apply_patch", + "shell", + "update_plan", + ]); const docsAllow = new Set(DOCS_TOOLS); const docsKept = proxies.filter((t) => docsAllow.has(t.definition.name)); - expect(docsKept.map((t) => t.definition.name)).toEqual(["apply_patch", "update_plan"]); + expect(docsKept.map((t) => t.definition.name)).toEqual([ + "apply_patch", + "update_plan", + ]); }); test("non-Codex runSubAgent-shaped mount produces no proxies at all", () => { @@ -171,8 +189,14 @@ describe("Codex tool proxy mount", () => { runTool: async () => ({ content: "ok" }), readRawFile: async () => ({ content: "ok" }), runManageTasks: async () => ({ content: "ok" }), - allowDelete: allowDeleteFromCapabilities({ mode: "allow", tools: BUILD_TOOLS }), - allowShell: allowShellFromCapabilities({ mode: "allow", tools: BUILD_TOOLS }), + allowDelete: allowDeleteFromCapabilities({ + mode: "allow", + tools: BUILD_TOOLS, + }), + allowShell: allowShellFromCapabilities({ + mode: "allow", + tools: BUILD_TOOLS, + }), }); expect(proxies).toEqual([]); }); diff --git a/src/agent/codex-tool-proxies.test.ts b/src/agent/codex-tool-proxies.test.ts index 525b9393e..f913350f6 100644 --- a/src/agent/codex-tool-proxies.test.ts +++ b/src/agent/codex-tool-proxies.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { createToolRunner } from "@intx/agent"; import type { AgentTool } from "@intx/agent"; @@ -66,7 +67,9 @@ function makeRecorder(initial: Record = {}): { return { calls, files, runTool, readRawFile }; } -const unusedManageTasks: CodexRunManageTasks = async () => ({ content: "unused" }); +const unusedManageTasks: CodexRunManageTasks = async () => ({ + content: "unused", +}); const unusedReadRawFile: CodexReadRawFile = async () => ({ content: "unused" }); // A real manage_tasks dispatch: parses with the actual arktype schema and @@ -103,9 +106,16 @@ async function invokeApplyPatch(tools: AgentTool[], input: string) { ); } -async function invokeTool(tools: AgentTool[], name: string, args: Record) { +async function invokeTool( + tools: AgentTool[], + name: string, + args: Record, +) { const runner = createToolRunner(tools); - return runner.run({ id: "call-1", name, arguments: args }, new AbortController().signal); + return runner.run( + { id: "call-1", name, arguments: args }, + new AbortController().signal, + ); } describe("createCodexToolProxies", () => { @@ -126,9 +136,13 @@ describe("createCodexToolProxies", () => { readRawFile: unusedReadRawFile, runManageTasks: unusedManageTasks, }); - expect(tools.map((t) => t.definition.name)).toEqual(["apply_patch", "shell", "update_plan"]); + expect(tools.map((t) => t.definition.name)).toEqual([ + "apply_patch", + "shell", + "update_plan", + ]); expect(tools.every((t) => t.kind === "string")).toBe(true); - expect(tools[0]!.definition.inputSchema).toMatchObject({ + expect(defined(tools[0]).definition.inputSchema).toMatchObject({ required: ["input"], }); }); @@ -162,7 +176,9 @@ describe("createCodexToolProxies", () => { }); test("delete forwards delete_file", async () => { - const { calls, files, runTool, readRawFile } = makeRecorder({ "obsolete.txt": "gone" }); + const { calls, files, runTool, readRawFile } = makeRecorder({ + "obsolete.txt": "gone", + }); const tools = createCodexToolProxies({ isCodex: true, runTool, @@ -177,13 +193,17 @@ describe("createCodexToolProxies", () => { `, ); expect(result.isError).toBeFalsy(); - expect(calls).toEqual([{ name: "delete_file", args: { path: "obsolete.txt" } }]); + expect(calls).toEqual([ + { name: "delete_file", args: { path: "obsolete.txt" } }, + ]); expect(files.has("obsolete.txt")).toBe(false); expect(result.content).toContain("Deleted file: obsolete.txt"); }); test("allowDelete false refuses Delete without calling delete_file", async () => { - const { calls, files, runTool, readRawFile } = makeRecorder({ "obsolete.txt": "gone" }); + const { calls, files, runTool, readRawFile } = makeRecorder({ + "obsolete.txt": "gone", + }); const tools = createCodexToolProxies({ isCodex: true, runTool, @@ -209,7 +229,9 @@ describe("createCodexToolProxies", () => { const original = `def greet(): print("Hi") `; - const { calls, files, runTool, readRawFile } = makeRecorder({ "src/app.py": original }); + const { calls, files, runTool, readRawFile } = makeRecorder({ + "src/app.py": original, + }); const tools = createCodexToolProxies({ isCodex: true, runTool, @@ -239,7 +261,9 @@ print("Hi") const original = `def greet(): print("Hi") `; - const { calls, files, runTool, readRawFile } = makeRecorder({ "src/app.py": original }); + const { calls, files, runTool, readRawFile } = makeRecorder({ + "src/app.py": original, + }); const tools = createCodexToolProxies({ isCodex: true, runTool, @@ -269,7 +293,9 @@ print("Hello, world!") print("Hi") print("bye") `; - const { calls, files, runTool, readRawFile } = makeRecorder({ "src/app.py": original }); + const { calls, files, runTool, readRawFile } = makeRecorder({ + "src/app.py": original, + }); const tools = createCodexToolProxies({ isCodex: true, runTool, @@ -288,7 +314,7 @@ print("bye") ); expect(result.isError).toBeFalsy(); expect(calls.map((c) => c.name)).toEqual(["write_file"]); - expect(calls[0]!.args.path).toBe("src/app.py"); + expect(defined(calls[0]).args.path).toBe("src/app.py"); expect(files.get("src/app.py")).toBe(`def greet(): print("Hello, world!") print("bye") @@ -299,7 +325,9 @@ print("bye") const original = `def greet(): print("Hi") `; - const { calls, files, runTool, readRawFile } = makeRecorder({ "src/app.py": original }); + const { calls, files, runTool, readRawFile } = makeRecorder({ + "src/app.py": original, + }); const tools = createCodexToolProxies({ isCodex: true, runTool, @@ -319,11 +347,11 @@ print("Hi") ); expect(result.isError).toBeFalsy(); expect(calls.map((c) => c.name)).toEqual(["write_file", "delete_file"]); - expect(calls[0]!.args.path).toBe("src/main.py"); - expect(calls[0]!.args.content).toBe(`def greet(): + expect(defined(calls[0]).args.path).toBe("src/main.py"); + expect(defined(calls[0]).args.content).toBe(`def greet(): print("Hello, world!") `); - expect(calls[1]!.args).toEqual({ path: "src/app.py" }); + expect(defined(calls[1]).args).toEqual({ path: "src/app.py" }); expect(files.has("src/app.py")).toBe(false); expect(files.get("src/main.py")).toBe(`def greet(): print("Hello, world!") @@ -355,7 +383,11 @@ print("Hello, world!") `, ); expect(result.isError).toBeFalsy(); - expect(calls.map((c) => c.name)).toEqual(["write_file", "write_file", "delete_file"]); + expect(calls.map((c) => c.name)).toEqual([ + "write_file", + "write_file", + "delete_file", + ]); expect(files.get("hello.txt")).toBe("Hello world\n"); expect(files.get("src/app.py")).toBe("new\n"); expect(files.has("obsolete.txt")).toBe(false); @@ -442,8 +474,12 @@ describe("shell proxy", () => { readRawFile, runManageTasks: unusedManageTasks, }); - await invokeTool(tools, "shell", { command: ["bash", "-lc", "echo 'hi there'"] }); - expect(calls).toEqual([{ name: "run_shell", args: { command: "echo 'hi there'" } }]); + await invokeTool(tools, "shell", { + command: ["bash", "-lc", "echo 'hi there'"], + }); + expect(calls).toEqual([ + { name: "run_shell", args: { command: "echo 'hi there'" } }, + ]); }); test("other argv arrays are shell-quoted and joined", async () => { @@ -455,7 +491,9 @@ describe("shell proxy", () => { runManageTasks: unusedManageTasks, }); await invokeTool(tools, "shell", { command: ["echo", "hello world"] }); - expect(calls).toEqual([{ name: "run_shell", args: { command: "echo 'hello world'" } }]); + expect(calls).toEqual([ + { name: "run_shell", args: { command: "echo 'hello world'" } }, + ]); }); test("workdir and timeout_ms translate to cwd and timeout", async () => { @@ -472,7 +510,10 @@ describe("shell proxy", () => { timeout_ms: 5000, }); expect(calls).toEqual([ - { name: "run_shell", args: { command: "pwd", cwd: "/tmp/work", timeout: 5000 } }, + { + name: "run_shell", + args: { command: "pwd", cwd: "/tmp/work", timeout: 5000 }, + }, ]); }); @@ -506,7 +547,10 @@ describe("shell proxy", () => { }); test("run_shell isError propagates as tool error", async () => { - const runTool: CodexRunTool = async () => ({ content: "boom", isError: true }); + const runTool: CodexRunTool = async () => ({ + content: "boom", + isError: true, + }); const tools = createCodexToolProxies({ isCodex: true, runTool, @@ -598,20 +642,36 @@ describe("update_plan proxy", () => { describe("allowDeleteFromCapabilities", () => { test("docs allowlist (includes delete_file) → true; build → true", () => { - expect(allowDeleteFromCapabilities({ mode: "allow", tools: DOCS_TOOLS })).toBe(true); - expect(allowDeleteFromCapabilities({ mode: "allow", tools: BUILD_TOOLS })).toBe(true); + expect( + allowDeleteFromCapabilities({ mode: "allow", tools: DOCS_TOOLS }), + ).toBe(true); + expect( + allowDeleteFromCapabilities({ mode: "allow", tools: BUILD_TOOLS }), + ).toBe(true); expect(allowDeleteFromCapabilities(undefined)).toBe(true); - expect(allowDeleteFromCapabilities({ mode: "exclude", tools: ["run_shell"] })).toBe(true); - expect(allowDeleteFromCapabilities({ mode: "exclude", tools: ["delete_file"] })).toBe(false); + expect( + allowDeleteFromCapabilities({ mode: "exclude", tools: ["run_shell"] }), + ).toBe(true); + expect( + allowDeleteFromCapabilities({ mode: "exclude", tools: ["delete_file"] }), + ).toBe(false); }); }); describe("allowShellFromCapabilities", () => { test("docs allowlist (no run_shell) → false; build → true", () => { - expect(allowShellFromCapabilities({ mode: "allow", tools: DOCS_TOOLS })).toBe(false); - expect(allowShellFromCapabilities({ mode: "allow", tools: BUILD_TOOLS })).toBe(true); + expect( + allowShellFromCapabilities({ mode: "allow", tools: DOCS_TOOLS }), + ).toBe(false); + expect( + allowShellFromCapabilities({ mode: "allow", tools: BUILD_TOOLS }), + ).toBe(true); expect(allowShellFromCapabilities(undefined)).toBe(true); - expect(allowShellFromCapabilities({ mode: "exclude", tools: ["delete_file"] })).toBe(true); - expect(allowShellFromCapabilities({ mode: "exclude", tools: ["run_shell"] })).toBe(false); + expect( + allowShellFromCapabilities({ mode: "exclude", tools: ["delete_file"] }), + ).toBe(true); + expect( + allowShellFromCapabilities({ mode: "exclude", tools: ["run_shell"] }), + ).toBe(false); }); }); diff --git a/src/agent/codex-tool-proxies.ts b/src/agent/codex-tool-proxies.ts index 8bb2628a3..20ae4e1bd 100644 --- a/src/agent/codex-tool-proxies.ts +++ b/src/agent/codex-tool-proxies.ts @@ -29,7 +29,9 @@ export type CodexRunTool = ( * output for model display, so it cannot supply the raw text * `applyUpdateHunks` needs to match a patch's context lines against (CL-6966). */ -export type CodexReadRawFile = (path: string) => Promise<{ content: string; isError?: boolean }>; +export type CodexReadRawFile = ( + path: string, +) => Promise<{ content: string; isError?: boolean }>; /** * Dispatches update_plan's translated call onto the real manage_tasks @@ -162,7 +164,9 @@ export const applyPatchDefinition: ToolDefinition = { * read_file); `shell` forwards onto `run_shell`; `update_plan` forwards onto * `manage_tasks`. */ -export function createCodexToolProxies(opts: CreateCodexToolProxiesOpts): AgentTool[] { +export function createCodexToolProxies( + opts: CreateCodexToolProxiesOpts, +): AgentTool[] { if (!opts.isCodex) return []; const allowDelete = opts.allowDelete !== false; const allowShell = opts.allowShell !== false; @@ -179,7 +183,9 @@ export function createCodexToolProxies(opts: CreateCodexToolProxiesOpts): AgentT * unconstrained / exclude-without-delete keep delete enabled. */ export function allowDeleteFromCapabilities( - capabilities: { mode: "allow" | "exclude"; tools: readonly string[] } | undefined, + capabilities: + | { mode: "allow" | "exclude"; tools: readonly string[] } + | undefined, ): boolean { if (capabilities === undefined) return true; if (capabilities.mode === "allow") { @@ -194,7 +200,9 @@ export function allowDeleteFromCapabilities( * instead of `delete_file` — docs leaves (DOCS_TOOLS omits run_shell) refuse. */ export function allowShellFromCapabilities( - capabilities: { mode: "allow" | "exclude"; tools: readonly string[] } | undefined, + capabilities: + | { mode: "allow" | "exclude"; tools: readonly string[] } + | undefined, ): boolean { if (capabilities === undefined) return true; if (capabilities.mode === "allow") { @@ -214,7 +222,9 @@ function createApplyPatchProxy( const parsed = ApplyPatchArgs(rawArgs); if (parsed instanceof type.errors) { // stringTool surfaces thrown errors as ToolResult.isError via createToolRunner. - throw new Error("Error: apply_patch requires a non-empty input (string)."); + throw new Error( + "Error: apply_patch requires a non-empty input (string).", + ); } let patch; @@ -230,7 +240,8 @@ function createApplyPatchProxy( const result = await applyOp(op, runTool, readRawFile, allowDelete); lines.push(result); } - if (lines.length === 0) return "apply_patch: no file operations in envelope."; + if (lines.length === 0) + return "apply_patch: no file operations in envelope."; return lines.join("\n"); }, }); @@ -255,7 +266,10 @@ async function applyOp( `apply_patch: Delete File is not allowed for this agent (delete_file capability missing): ${op.path}`, ); } - return requireOk(await runTool("delete_file", { path: op.path }), `delete ${op.path}`); + return requireOk( + await runTool("delete_file", { path: op.path }), + `delete ${op.path}`, + ); } // update (+ optional move): read → applyUpdateHunks → write (to moveTo or path) @@ -298,7 +312,10 @@ async function applyOp( return writeMsg; } -function requireOk(result: { content: string; isError?: boolean }, label: string): string { +function requireOk( + result: { content: string; isError?: boolean }, + label: string, +): string { if (result.isError === true) { throw new Error(`${label} failed: ${result.content}`); } @@ -325,7 +342,10 @@ export const shellDefinition: ToolDefinition = { description: 'The command to run, as a shell string or an argv array (e.g. ["bash","-lc","ls"]).', }, - workdir: { type: "string", description: "Working directory for the command." }, + workdir: { + type: "string", + description: "Working directory for the command.", + }, timeout_ms: { type: "number", description: "Timeout in milliseconds." }, }, required: ["command"], @@ -349,28 +369,42 @@ function shellQuote(arg: string): string { */ function normalizeShellCommand(command: string | string[]): string { if (typeof command === "string") return command; + const wrapper = command[0]; + const flag = command[1]; + const script = command[2]; if ( command.length === 3 && - SHELL_WRAPPERS.has(command[0]!.replace(/^.*\//, "")) && - (command[1] === "-lc" || command[1] === "-c") + wrapper !== undefined && + script !== undefined && + SHELL_WRAPPERS.has(wrapper.replace(/^.*\//, "")) && + (flag === "-lc" || flag === "-c") ) { - return command[2]!; + return script; } return command.map(shellQuote).join(" "); } -function createShellProxy(runTool: CodexRunTool, allowShell: boolean): AgentTool { +function createShellProxy( + runTool: CodexRunTool, + allowShell: boolean, +): AgentTool { return stringTool({ definition: shellDefinition, handler: async (rawArgs: Record): Promise => { const parsed = ShellArgs(rawArgs); if (parsed instanceof type.errors) { - throw new Error("Error: shell requires a command (string or string[])."); + throw new Error( + "Error: shell requires a command (string or string[]).", + ); } if (!allowShell) { - throw new Error("shell: not allowed for this agent (run_shell capability missing)."); + throw new Error( + "shell: not allowed for this agent (run_shell capability missing).", + ); } - const args: Record = { command: normalizeShellCommand(parsed.command) }; + const args: Record = { + command: normalizeShellCommand(parsed.command), + }; if (parsed.workdir !== undefined) args.cwd = parsed.workdir; if (parsed.timeout_ms !== undefined) args.timeout = parsed.timeout_ms; return requireOk(await runTool("run_shell", args), "shell"); @@ -417,7 +451,9 @@ export const updatePlanDefinition: ToolDefinition = { }, }; -function codexPlanStatusToTaskStatus(status: typeof CodexPlanStatus.infer): TaskStatus { +function codexPlanStatusToTaskStatus( + status: typeof CodexPlanStatus.infer, +): TaskStatus { if (status === "pending") return "todo"; if (status === "in_progress") return "doing"; return "done"; @@ -429,7 +465,9 @@ function createUpdatePlanProxy(runManageTasks: CodexRunManageTasks): AgentTool { handler: async (rawArgs: Record): Promise => { const parsed = UpdatePlanArgs(rawArgs); if (parsed instanceof type.errors) { - throw new Error("Error: update_plan requires a plan array of { step, status }."); + throw new Error( + "Error: update_plan requires a plan array of { step, status }.", + ); } // manage_tasks has no "cancelled" equivalent in Codex's plan shape // (pending/in_progress/completed) — this proxy never produces it, so a @@ -441,7 +479,10 @@ function createUpdatePlanProxy(runManageTasks: CodexRunManageTasks): AgentTool { title: item.step, status: codexPlanStatusToTaskStatus(item.status), })); - return requireOk(await runManageTasks({ action: "create", tasks }), "update_plan"); + return requireOk( + await runManageTasks({ action: "create", tasks }), + "update_plan", + ); }, }); } diff --git a/src/agent/compaction.test.ts b/src/agent/compaction.test.ts index 8a35a790e..99d78c742 100644 --- a/src/agent/compaction.test.ts +++ b/src/agent/compaction.test.ts @@ -7,7 +7,10 @@ import type { TokenUsage, } from "@intx/types/runtime"; import { createCompactionGovernor } from "./compaction.js"; -import { compactionResumeDeltaFor, compactionThresholdFor } from "../provider/context-window.js"; +import { + compactionResumeDeltaFor, + compactionThresholdFor, +} from "../provider/context-window.js"; import { COMPACTOR_KEEP_RECENT_TURNS, COMPACT_SPACER_TEXT, @@ -16,8 +19,15 @@ import { } from "../session/compactor.js"; const capabilities = { - infer: (options?: unknown) => ({ type: "infer", ...(options !== undefined ? { options } : {}) }), - compact: (compactor: string, reason: string) => ({ type: "compact", compactor, reason }), + infer: (options?: unknown) => ({ + type: "infer", + ...(options !== undefined ? { options } : {}), + }), + compact: (compactor: string, reason: string) => ({ + type: "compact", + compactor, + reason, + }), } as unknown as ReactorCapabilities; // Distinct, non-zero cacheRead/cacheWrite so a test asserting on the total @@ -64,14 +74,24 @@ function inferenceDoneWithTools( type: "inference.done", turn: { role: "assistant", - content: [{ type: "tool_call", id: "c1", name: "read_file", arguments: { path: "a.ts" } }], + content: [ + { + type: "tool_call", + id: "c1", + name: "read_file", + arguments: { path: "a.ts" }, + }, + ], }, usage: usage(input), source: { sourceId: "s", provider: "p", model: "m" }, } as unknown as Extract; } -function inferenceDoneWithoutUsage(): Extract { +function inferenceDoneWithoutUsage(): Extract< + ReactorInboundEvent, + { type: "inference.done" } +> { return { type: "inference.done", turn: { role: "assistant", content: [{ type: "text", text: "ok" }] }, @@ -80,7 +100,10 @@ function inferenceDoneWithoutUsage(): Extract; } -function inferenceDoneMissingUsage(): Extract { +function inferenceDoneMissingUsage(): Extract< + ReactorInboundEvent, + { type: "inference.done" } +> { return { type: "inference.done", turn: { role: "assistant", content: [{ type: "text", text: "ok" }] }, @@ -122,7 +145,11 @@ describe("compaction governor", () => { const governor = createCompactionGovernor(() => continuations++); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + const actions = governor.interceptActions( + toolDone(), + inferAction, + capabilities, + ); expect(actions).not.toBeNull(); expect(actions?.some((a) => a.type === "compact")).toBe(true); expect(actions?.some((a) => a.type === "infer")).toBe(false); @@ -133,38 +160,56 @@ describe("compaction governor", () => { }); test("stays inert below the threshold or with few turns", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(1000), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); governor.noteInferenceDone(inferenceDone(overThreshold), threeTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); }); test("disarms a sticky pending when a later measurement falls under threshold", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); // Under-threshold follow-up must clear pending, not leave it armed. governor.noteInferenceDone(inferenceDone(1000), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); }); test("stays inert without a continuation channel", () => { const governor = createCompactionGovernor(undefined); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); - expect(governor.interceptOverflow(overflowError(), capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); + expect( + governor.interceptOverflow(overflowError(), capabilities), + ).toBeNull(); }); test("recovers from context overflow a bounded number of times", () => { - const governor = createCompactionGovernor(() => {}); - expect(governor.interceptOverflow(overflowError(), capabilities)).not.toBeNull(); + const governor = createCompactionGovernor(() => undefined); + expect( + governor.interceptOverflow(overflowError(), capabilities), + ).not.toBeNull(); expect(governor.resumeAfterCompact(emptyMessage())).toBe("infer"); - expect(governor.interceptOverflow(overflowError(), capabilities)).not.toBeNull(); - expect(governor.interceptOverflow(overflowError(), capabilities)).toBeNull(); + expect( + governor.interceptOverflow(overflowError(), capabilities), + ).not.toBeNull(); + expect( + governor.interceptOverflow(overflowError(), capabilities), + ).toBeNull(); governor.noteInferenceDone(inferenceDone(1000), tenTurns); - expect(governor.interceptOverflow(overflowError(), capabilities)).not.toBeNull(); + expect( + governor.interceptOverflow(overflowError(), capabilities), + ).not.toBeNull(); }); test("an idle over-threshold turn requests a continuation and compacts on its arrival", () => { @@ -179,12 +224,21 @@ describe("compaction governor", () => { governor.noteIdleTurn(inferenceDone(overThreshold), terminal); expect(continuations).toBe(1); - const actions = governor.interceptIdleContinuation(emptyMessage(), capabilities); + const actions = governor.interceptIdleContinuation( + emptyMessage(), + capabilities, + ); expect(actions).toEqual([ - { type: "compact", compactor: "pruning-compactor", reason: "context-threshold" }, + { + type: "compact", + compactor: "pruning-compactor", + reason: "context-threshold", + }, ] as ReactorAction[]); // The continuation was consumed; nothing further is intercepted. - expect(governor.interceptIdleContinuation(emptyMessage(), capabilities)).toBeNull(); + expect( + governor.interceptIdleContinuation(emptyMessage(), capabilities), + ).toBeNull(); }); // Idle compact with an empty continuation previously left postCompactInfer @@ -198,12 +252,21 @@ describe("compaction governor", () => { expect(governor.usingEstimate).toBe(false); const before = governor.estimatedTokens; - governor.noteIdleTurn(inferenceDone(overThreshold), [{ type: "reply", content: "done" }]); + governor.noteIdleTurn(inferenceDone(overThreshold), [ + { type: "reply", content: "done" }, + ]); expect(continuations).toBe(1); - const actions = governor.interceptIdleContinuation(emptyMessage(), capabilities); + const actions = governor.interceptIdleContinuation( + emptyMessage(), + capabilities, + ); expect(actions).toEqual([ - { type: "compact", compactor: "pruning-compactor", reason: "context-threshold" }, + { + type: "compact", + compactor: "pruning-compactor", + reason: "context-threshold", + }, ] as ReactorAction[]); // A second continuation re-enters decide after the compact cycle so the // governor can adopt the shrunk turns — without starting a new inference. @@ -223,7 +286,9 @@ describe("compaction governor", () => { let continuations = 0; const governor = createCompactionGovernor(() => continuations++); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - governor.noteIdleTurn(inferenceDone(overThreshold), [{ type: "reply", content: "done" }]); + governor.noteIdleTurn(inferenceDone(overThreshold), [ + { type: "reply", content: "done" }, + ]); const raced = { type: "message.received", @@ -241,7 +306,9 @@ describe("compaction governor", () => { let continuations = 0; const governor = createCompactionGovernor(() => continuations++); - governor.noteIdleTurn(inferenceDone(1000), [{ type: "reply", content: "x" }]); + governor.noteIdleTurn(inferenceDone(1000), [ + { type: "reply", content: "x" }, + ]); expect(continuations).toBe(0); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); @@ -250,63 +317,84 @@ describe("compaction governor", () => { { type: "infer" }, ]); expect(continuations).toBe(0); - expect(governor.interceptIdleContinuation(emptyMessage(), capabilities)).toBeNull(); + expect( + governor.interceptIdleContinuation(emptyMessage(), capabilities), + ).toBeNull(); }); test("arms from the running local estimate when usage is zero", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); const overThresholdChars = (compactionThresholdFor("m") + 1) * 4; const turns = turnsOfLength(10, Math.ceil(overThresholdChars / 10)); governor.noteInferenceDone(inferenceDoneWithoutUsage(), turns); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + const actions = governor.interceptActions( + toolDone(), + inferAction, + capabilities, + ); expect(actions).not.toBeNull(); expect(actions?.some((a) => a.type === "compact")).toBe(true); }); test("arms from the running local estimate when usage is omitted", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); const overThresholdChars = (compactionThresholdFor("m") + 1) * 4; const turns = turnsOfLength(10, Math.ceil(overThresholdChars / 10)); governor.noteInferenceDone(inferenceDoneMissingUsage(), turns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); }); test("stays inert when usage is missing but the accumulated estimate is small", () => { - const governor = createCompactionGovernor(() => {}); - governor.noteInferenceDone(inferenceDoneWithoutUsage(), turnsOfLength(10, 4)); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + const governor = createCompactionGovernor(() => undefined); + governor.noteInferenceDone( + inferenceDoneWithoutUsage(), + turnsOfLength(10, 4), + ); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); }); test("arms from accumulated growth across many turns when usage is absent", () => { // A single turn's content stays well under the threshold; only the sum // across a long conversation crosses it. Measuring the latest turn alone // would never arm here. - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); const perTurnChars = 2000; const turns = turnsOfLength(200, perTurnChars); governor.noteInferenceDone(inferenceDoneWithoutUsage(), turns); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + const actions = governor.interceptActions( + toolDone(), + inferAction, + capabilities, + ); expect(actions).not.toBeNull(); expect(actions?.some((a) => a.type === "compact")).toBe(true); }); test("prefers provider usage over the local estimate when usage is present", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); // Local estimate is huge; reported usage is small. Prefer the provider. const hugeTurns = turnsOfLength(200, 2000); governor.noteInferenceDone(inferenceDone(1000), hugeTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); // Provider reports over threshold with a small local estimate → arm. governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); }); test("syncFromTurns keeps the running estimate current outside arming", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); expect(governor.estimatedTokens).toBe(0); const turns = turnsOfLength(4, 40); @@ -319,13 +407,21 @@ describe("compaction governor", () => { }); test("only intercepts on tool.done with a pending infer", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); expect( - governor.interceptActions(inferenceDone(overThreshold), inferAction, capabilities), + governor.interceptActions( + inferenceDone(overThreshold), + inferAction, + capabilities, + ), ).toBeNull(); expect( - governor.interceptActions(toolDone(), [{ type: "reply", content: "x" }], capabilities), + governor.interceptActions( + toolDone(), + [{ type: "reply", content: "x" }], + capabilities, + ), ).toBeNull(); }); @@ -333,9 +429,14 @@ describe("compaction governor", () => { // Two turns is well under MIN_TURNS_TO_COMPACT. createPruningCompactor // no-ops at the same floor (see session/compactor.ts), so arming here // would spend a reactor cycle that cannot shrink anything. - const governor = createCompactionGovernor(() => {}); - governor.noteInferenceDone(inferenceDone(overThreshold * 10), turnsOfLength(2, 1)); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + const governor = createCompactionGovernor(() => undefined); + governor.noteInferenceDone( + inferenceDone(overThreshold * 10), + turnsOfLength(2, 1), + ); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); }); test("arms on tool.done from a live estimate even when the last snapshot was under threshold", () => { @@ -343,14 +444,22 @@ describe("compaction governor", () => { // starts small and stays false), but the tool result that follows is // itself large enough to cross the ordinary threshold before the next // inference.done ever runs. - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDoneWithoutUsage(), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); const overThresholdChars = (compactionThresholdFor("m") + 1) * 4; - governor.syncFromTurns(turnsOfLength(10, Math.ceil(overThresholdChars / 10))); + governor.syncFromTurns( + turnsOfLength(10, Math.ceil(overThresholdChars / 10)), + ); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + const actions = governor.interceptActions( + toolDone(), + inferAction, + capabilities, + ); expect(actions).not.toBeNull(); expect(actions?.some((a) => a.type === "compact")).toBe(true); }); @@ -360,16 +469,28 @@ describe("compaction governor", () => { // compactorNoOpFloor(COMPACTOR_KEEP_RECENT_TURNS). Arming at or below it // would spend a reactor cycle that is guaranteed to shrink nothing. const floor = compactorNoOpFloor(COMPACTOR_KEEP_RECENT_TURNS); - const governor = createCompactionGovernor(() => {}); - governor.noteInferenceDone(inferenceDone(overThreshold), turnsOfLength(floor, 1)); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + const governor = createCompactionGovernor(() => undefined); + governor.noteInferenceDone( + inferenceDone(overThreshold), + turnsOfLength(floor, 1), + ); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); }); test("arms one turn past the floor createPruningCompactor no-ops on", () => { const floor = compactorNoOpFloor(COMPACTOR_KEEP_RECENT_TURNS); - const governor = createCompactionGovernor(() => {}); - governor.noteInferenceDone(inferenceDone(overThreshold), turnsOfLength(floor + 1, 1)); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + const governor = createCompactionGovernor(() => undefined); + governor.noteInferenceDone( + inferenceDone(overThreshold), + turnsOfLength(floor + 1, 1), + ); + const actions = governor.interceptActions( + toolDone(), + inferAction, + capabilities, + ); expect(actions).not.toBeNull(); expect(actions?.some((a) => a.type === "compact")).toBe(true); }); @@ -382,20 +503,26 @@ describe("compaction governor", () => { // authoritative until the next inference.done — a huge tool result // arriving in between is not caught until then, unlike the // usage-omitted case covered above. - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(1000), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); const overThresholdChars = (compactionThresholdFor("m") + 1) * 4; - governor.syncFromTurns(turnsOfLength(10, Math.ceil(overThresholdChars / 10))); + governor.syncFromTurns( + turnsOfLength(10, Math.ceil(overThresholdChars / 10)), + ); // Still null: the live estimate is now over threshold, but the last // arming decision trusted reported usage, so it is not re-checked here. - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); }); test("notePostCompact syncs the shrunk turns and keeps the estimate authoritative until the next inference.done", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); const large = turnsOfLength(10, 200); governor.noteInferenceDone(inferenceDone(overThreshold), large); expect(governor.usingEstimate).toBe(false); @@ -414,55 +541,84 @@ describe("compaction governor", () => { }); test("does not re-arm after a compact that remains over the high watermark", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); // Post-compact snapshot is still over high; growth hysteresis must hold // the next arm until usage grows by resumeDelta. governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); }); test("re-arms after usage grows by the resume delta past the last compact", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); - governor.noteInferenceDone(inferenceDone(overThreshold + resumeDelta), tenTurns); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + governor.noteInferenceDone( + inferenceDone(overThreshold + resumeDelta), + tenTurns, + ); + const actions = governor.interceptActions( + toolDone(), + inferAction, + capabilities, + ); expect(actions).not.toBeNull(); expect(actions?.some((a) => a.type === "compact")).toBe(true); }); test("clears hysteresis once usage drops under the high watermark", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); governor.noteInferenceDone(inferenceDone(1000), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); // Next crossing of high arms immediately — no growth delta required. governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + const actions = governor.interceptActions( + toolDone(), + inferAction, + capabilities, + ); expect(actions).not.toBeNull(); expect(actions?.some((a) => a.type === "compact")).toBe(true); }); test("overflow still compact while hysteresis blocks the proactive path", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); governor.noteInferenceDone(inferenceDone(overThreshold), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); const actions = governor.interceptOverflow(overflowError(), capabilities); expect(actions).not.toBeNull(); @@ -470,46 +626,77 @@ describe("compaction governor", () => { }); test("consecutive threshold and idle compacts are bounded until occupancy", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); const echo = LEGACY_COMPACT_SPACER_TEXT; governor.noteInferenceDone(inferenceDone(overThreshold, echo), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); governor.noteInferenceDone(inferenceDone(overThreshold, echo), tenTurns); - governor.noteInferenceDone(inferenceDone(overThreshold + resumeDelta, echo), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + governor.noteInferenceDone( + inferenceDone(overThreshold + resumeDelta, echo), + tenTurns, + ); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); - governor.noteInferenceDone(inferenceDone(overThreshold + resumeDelta, echo), tenTurns); - governor.noteInferenceDone(inferenceDone(overThreshold + 2 * resumeDelta, echo), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); - governor.noteIdleTurn(inferenceDone(overThreshold + 2 * resumeDelta, echo), [ - { type: "reply", content: "done" }, - ]); - expect(governor.interceptIdleContinuation(emptyMessage(), capabilities)).toBeNull(); + governor.noteInferenceDone( + inferenceDone(overThreshold + resumeDelta, echo), + tenTurns, + ); + governor.noteInferenceDone( + inferenceDone(overThreshold + 2 * resumeDelta, echo), + tenTurns, + ); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); + governor.noteIdleTurn( + inferenceDone(overThreshold + 2 * resumeDelta, echo), + [{ type: "reply", content: "done" }], + ); + expect( + governor.interceptIdleContinuation(emptyMessage(), capabilities), + ).toBeNull(); governor.noteInferenceDone( inferenceDone(overThreshold + 3 * resumeDelta, "real work"), tenTurns, ); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).toBeNull(); - governor.noteInferenceDone(inferenceDoneWithTools(overThreshold + 4 * resumeDelta), tenTurns); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).not.toBeNull(); + governor.noteInferenceDone( + inferenceDoneWithTools(overThreshold + 4 * resumeDelta), + tenTurns, + ); + expect( + governor.interceptActions(toolDone(), inferAction, capabilities), + ).not.toBeNull(); }); test("spacer-echo terminal does not arm idle compact", () => { let continuations = 0; const governor = createCompactionGovernor(() => continuations++); - governor.noteInferenceDone(inferenceDone(overThreshold, LEGACY_COMPACT_SPACER_TEXT), tenTurns); - governor.noteIdleTurn(inferenceDone(overThreshold, LEGACY_COMPACT_SPACER_TEXT), [ - { type: "reply", content: LEGACY_COMPACT_SPACER_TEXT }, - ]); + governor.noteInferenceDone( + inferenceDone(overThreshold, LEGACY_COMPACT_SPACER_TEXT), + tenTurns, + ); + governor.noteIdleTurn( + inferenceDone(overThreshold, LEGACY_COMPACT_SPACER_TEXT), + [{ type: "reply", content: LEGACY_COMPACT_SPACER_TEXT }], + ); expect(continuations).toBe(0); governor.noteIdleTurn(inferenceDone(overThreshold, COMPACT_SPACER_TEXT), [ { type: "reply", content: COMPACT_SPACER_TEXT }, ]); expect(continuations).toBe(0); - expect(governor.interceptIdleContinuation(emptyMessage(), capabilities)).toBeNull(); + expect( + governor.interceptIdleContinuation(emptyMessage(), capabilities), + ).toBeNull(); governor.noteIdleTurn(inferenceDone(overThreshold, "done"), [ { type: "reply", content: "done" }, diff --git a/src/agent/compaction.ts b/src/agent/compaction.ts index 720ae5124..90ad892ce 100644 --- a/src/agent/compaction.ts +++ b/src/agent/compaction.ts @@ -16,7 +16,10 @@ import { compactorNoOpFloor, isCompactSpacerEchoTurn, } from "../session/compactor.js"; -import { createContextEstimate, estimateOverheadTokens } from "./context-estimate.js"; +import { + createContextEstimate, + estimateOverheadTokens, +} from "./context-estimate.js"; import { onTurnBoundary } from "./reactor-events.js"; const COMPACTOR_NAME = "pruning-compactor"; @@ -78,7 +81,9 @@ export function createCompactionGovernor( // or report zero leave the proactive path blind; the estimate fills that // gap. When the provider reports real usage we prefer it so a coarse local // count cannot thrash against a trustworthy signal. - const estimate = createContextEstimate(estimateOverheadTokens(systemPrompt, toolDefinitions)); + const estimate = createContextEstimate( + estimateOverheadTokens(systemPrompt, toolDefinitions), + ); // Re-sync after turn appends, tool results, and compaction rewrites. Callers // pass the full turn list so the estimate stays accurate without incremental @@ -93,7 +98,10 @@ export function createCompactionGovernor( const high = compactionThresholdFor(lastModel); if (contextTokens <= high) return false; if (tokensAtLastCompact !== undefined) { - return contextTokens >= tokensAtLastCompact + compactionResumeDeltaFor(lastModel); + return ( + contextTokens >= + tokensAtLastCompact + compactionResumeDeltaFor(lastModel) + ); } return true; } @@ -111,12 +119,18 @@ export function createCompactionGovernor( noteCompactIssued(); } - function isSpacerEchoTerminal(event: ReactorInboundEvent, actions: ReactorAction[]): boolean { + function isSpacerEchoTerminal( + event: ReactorInboundEvent, + actions: ReactorAction[], + ): boolean { // Fail-closed only. ChatDirector owns spacer-echo completeness (nudge, then // loop-protection / workflow / open-task rails). This just refuses to treat // that incomplete wait or reply as an idle-compact pause. - if (event.type === "inference.done" && isCompactSpacerEchoTurn(event.turn)) return true; - return actions.some((a) => a.type === "reply" && assistantTextIsCompactSpacerEcho(a.content)); + if (event.type === "inference.done" && isCompactSpacerEchoTurn(event.turn)) + return true; + return actions.some( + (a) => a.type === "reply" && assistantTextIsCompactSpacerEcho(a.content), + ); } function noteInferenceDone( @@ -169,7 +183,8 @@ export function createCompactionGovernor( capabilities: ReactorCapabilities, ): ReactorAction[] | null { if (event.type !== "tool.done") return null; - if (!pending && !(usingEstimate && isOverThreshold(estimate.tokens))) return null; + if (!pending && !(usingEstimate && isOverThreshold(estimate.tokens))) + return null; if (!actions.some((a) => a.type === "infer")) return null; if (atThresholdCompactCap()) return null; pending = false; @@ -186,7 +201,10 @@ export function createCompactionGovernor( // pending compaction would wait indefinitely for the next tool batch. When // the turn ends without follow-up work, ask the host for a continuation and // compact when it (or the operator's next message) arrives. - function noteIdleTurn(event: ReactorInboundEvent, actions: ReactorAction[]): void { + function noteIdleTurn( + event: ReactorInboundEvent, + actions: ReactorAction[], + ): void { if (!pending || idlePending || requestContinuation === undefined) return; if (atThresholdCompactCap()) return; if (!onTurnBoundary(event)) return; @@ -210,7 +228,8 @@ export function createCompactionGovernor( } idlePending = false; pending = false; - const content = typeof event.message.content === "string" ? event.message.content : ""; + const content = + typeof event.message.content === "string" ? event.message.content : ""; // The reactor delivers no event after compact, so always request a // continuation to re-enter decide against the shrunk turns: // - raced operator content → re-infer to answer it @@ -233,7 +252,10 @@ export function createCompactionGovernor( capabilities: ReactorCapabilities, ): ReactorAction[] | null { if (requestContinuation === undefined) return null; - if (event.type !== "inference.error" || event.error.category !== "context_overflow") { + if ( + event.type !== "inference.error" || + event.error.category !== "context_overflow" + ) { return null; } if (overflowRecoveries >= MAX_OVERFLOW_RECOVERIES) return null; @@ -248,9 +270,12 @@ export function createCompactionGovernor( // After compact, a content-less continuation re-enters decide. "infer" means // resume the interrupted loop; "meter" means adopt the shrunk turns for the // Ctx display and stay idle (idle empty compact has nothing to answer). - function resumeAfterCompact(event: ReactorInboundEvent): "infer" | "meter" | null { + function resumeAfterCompact( + event: ReactorInboundEvent, + ): "infer" | "meter" | null { if (event.type !== "message.received") return null; - const content = typeof event.message.content === "string" ? event.message.content : ""; + const content = + typeof event.message.content === "string" ? event.message.content : ""; if (content.length > 0) return null; if (postCompactInfer) { postCompactInfer = false; diff --git a/src/agent/context-estimate.test.ts b/src/agent/context-estimate.test.ts index d69b20937..ea956bc53 100644 --- a/src/agent/context-estimate.test.ts +++ b/src/agent/context-estimate.test.ts @@ -14,7 +14,10 @@ import { estimateTokensFromChars, } from "./context-estimate.js"; -function textTurn(text: string, role: "user" | "assistant" = "user"): ConversationTurn { +function textTurn( + text: string, + role: "user" | "assistant" = "user", +): ConversationTurn { return { role, content: [{ type: "text", text }], @@ -34,8 +37,14 @@ describe("estimateTokensFromChars", () => { describe("estimateMediaSourceTokens", () => { test("counts base64 payload chars and floors external references", () => { - const base64: MediaSource = { kind: "base64", data: "abcd".repeat(100), mimeType: "image/png" }; - expect(estimateMediaSourceTokens(base64)).toBe(estimateTokensFromChars(400)); + const base64: MediaSource = { + kind: "base64", + data: "abcd".repeat(100), + mimeType: "image/png", + }; + expect(estimateMediaSourceTokens(base64)).toBe( + estimateTokensFromChars(400), + ); const url: MediaSource = { kind: "url", @@ -53,7 +62,9 @@ describe("estimateMediaSourceTokens", () => { mimeType: "image/png", }; expect(estimateMediaSourceTokens(huge)).toBe(2_500); - expect(estimateMediaSourceTokens(huge)).toBeLessThan(estimateTokensFromChars(1_000_000)); + expect(estimateMediaSourceTokens(huge)).toBeLessThan( + estimateTokensFromChars(1_000_000), + ); }); }); @@ -69,7 +80,9 @@ describe("estimateContentBlockTokens", () => { arguments: { command: "ls" }, } as ContentBlock; expect(estimateContentBlockTokens(toolCall)).toBe( - estimateTokensFromChars("run_shell".length + JSON.stringify({ command: "ls" }).length), + estimateTokensFromChars( + "run_shell".length + JSON.stringify({ command: "ls" }).length, + ), ); const toolResult: ContentBlock = { @@ -77,7 +90,9 @@ describe("estimateContentBlockTokens", () => { callId: "c1", content: [{ type: "text", text: "ok" }], } as ContentBlock; - expect(estimateContentBlockTokens(toolResult)).toBe(estimateTokensFromChars(2)); + expect(estimateContentBlockTokens(toolResult)).toBe( + estimateTokensFromChars(2), + ); const image: ContentBlock = { type: "image", @@ -99,10 +114,17 @@ describe("estimateOverheadTokens", () => { test("counts the system prompt and every tool's name, description, and schema", () => { const systemPrompt = "x".repeat(40); const tools: ToolDefinition[] = [ - { name: "run_shell", description: "y".repeat(20), inputSchema: { command: "string" } }, + { + name: "run_shell", + description: "y".repeat(20), + inputSchema: { command: "string" }, + }, ]; const expectedChars = - 40 + "run_shell".length + 20 + JSON.stringify({ command: "string" }).length; + 40 + + "run_shell".length + + 20 + + JSON.stringify({ command: "string" }).length; expect(estimateOverheadTokens(systemPrompt, tools)).toBe( estimateTokensFromChars(expectedChars), ); @@ -188,7 +210,9 @@ describe("createContextEstimate", () => { expect(estimate.syncFromTurns([first, second])).toBe(3); const rewritten = [textTurn("xxxx"), textTurn("yyyyyyyy", "assistant")]; - expect(estimate.syncFromTurns(rewritten)).toBe(estimateContextTokens(rewritten)); + expect(estimate.syncFromTurns(rewritten)).toBe( + estimateContextTokens(rewritten), + ); expect(estimate.tokens).toBe(3); expect(estimate.turnCount).toBe(2); diff --git a/src/agent/context-estimate.ts b/src/agent/context-estimate.ts index ce496f345..cc7a688da 100644 --- a/src/agent/context-estimate.ts +++ b/src/agent/context-estimate.ts @@ -29,7 +29,10 @@ export function estimateTokensFromChars(chars: number): number { export function estimateMediaSourceTokens(source: MediaSource): number { if (source.kind === "base64") { - return Math.min(estimateTokensFromChars(source.data.length), MEDIA_BASE64_MAX_TOKENS); + return Math.min( + estimateTokensFromChars(source.data.length), + MEDIA_BASE64_MAX_TOKENS, + ); } return MEDIA_REFERENCE_FLOOR_TOKENS; } @@ -45,9 +48,14 @@ export function estimateContentBlockTokens(block: ContentBlock): number { case "refusal": return estimateTokensFromChars(block.reason.length); case "tool_call": - return estimateTokensFromChars(block.name.length + JSON.stringify(block.arguments).length); + return estimateTokensFromChars( + block.name.length + JSON.stringify(block.arguments).length, + ); case "tool_result": - return block.content.reduce((sum, part) => sum + estimateContentBlockTokens(part), 0); + return block.content.reduce( + (sum, part) => sum + estimateContentBlockTokens(part), + 0, + ); case "image": case "audio": case "video": @@ -60,7 +68,9 @@ export function estimateContentBlockTokens(block: ContentBlock): number { (block.source.uri?.length ?? 0), ); case "code_execution_request": - return estimateTokensFromChars(block.code.length + (block.language?.length ?? 0)); + return estimateTokensFromChars( + block.code.length + (block.language?.length ?? 0), + ); case "code_execution_result": return estimateTokensFromChars( (block.stdout?.length ?? 0) + @@ -80,7 +90,9 @@ function estimateTurnTokens(turn: ConversationTurn): number { return total; } -export function estimateContextTokens(turns: readonly ConversationTurn[]): number { +export function estimateContextTokens( + turns: readonly ConversationTurn[], +): number { let total = 0; for (const turn of turns ?? []) { total += estimateTurnTokens(turn); @@ -99,7 +111,10 @@ export function estimateOverheadTokens( ): number { let chars = systemPrompt.length; for (const tool of toolDefinitions) { - chars += tool.name.length + tool.description.length + JSON.stringify(tool.inputSchema).length; + chars += + tool.name.length + + tool.description.length + + JSON.stringify(tool.inputSchema).length; } return estimateTokensFromChars(chars); } diff --git a/src/agent/context-extensions.ts b/src/agent/context-extensions.ts index e105a5a10..85538d6f0 100644 --- a/src/agent/context-extensions.ts +++ b/src/agent/context-extensions.ts @@ -1,7 +1,9 @@ import { join } from "node:path"; import { SETTINGS_DIR_NAME } from "../branding.js"; -export async function loadAgentContextExtensions(cwd: string): Promise { +export async function loadAgentContextExtensions( + cwd: string, +): Promise { const extensions: string[] = []; const agentsMdPath = join(cwd, "AGENTS.md"); try { @@ -23,7 +25,9 @@ export async function loadAgentContextExtensions(cwd: string): Promise } } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code !== "ENOENT") { - process.stderr.write(`[interchange] Warning: could not read AGENTS.md: ${String(err)}\n`); + process.stderr.write( + `[interchange] Warning: could not read AGENTS.md: ${String(err)}\n`, + ); } } return extensions; @@ -39,7 +43,9 @@ export interface SystemPromptOverrides { // Project-level system-prompt overrides, resolved repo-root first then .corbits/. // SYSTEM.md replaces the base block; APPEND_SYSTEM.md is appended. Mirrors Pi's // SYSTEM.md / APPEND_SYSTEM.md convention. -export async function loadSystemPromptOverrides(cwd: string): Promise { +export async function loadSystemPromptOverrides( + cwd: string, +): Promise { const dirs = [cwd, join(cwd, SETTINGS_DIR_NAME)]; const base = await firstFile(dirs, "SYSTEM.md"); const appendBody = await firstFile(dirs, "APPEND_SYSTEM.md"); @@ -49,14 +55,19 @@ export async function loadSystemPromptOverrides(cwd: string): Promise { +async function firstFile( + dirs: string[], + name: string, +): Promise { for (const dir of dirs) { try { const content = (await Bun.file(join(dir, name)).text()).trim(); if (content.length > 0) return content; } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code !== "ENOENT") { - process.stderr.write(`[interchange] Warning: could not read ${name}: ${String(err)}\n`); + process.stderr.write( + `[interchange] Warning: could not read ${name}: ${String(err)}\n`, + ); } } } diff --git a/src/agent/director.test.ts b/src/agent/director.test.ts index 6cc6c3db5..8113bf217 100644 --- a/src/agent/director.test.ts +++ b/src/agent/director.test.ts @@ -12,15 +12,26 @@ const mockState: ReactorState = { turns: [] } as unknown as ReactorState; function makeCapabilities(): ReactorCapabilities { return { infer: (options) => - ({ type: "infer", ...(options !== undefined ? { options } : {}) }) as ReactorAction, + ({ + type: "infer", + ...(options !== undefined ? { options } : {}), + }) as ReactorAction, executeTools: (calls, parallel, addToHistory) => - ({ type: "execute_tools", calls, parallel, addToHistory }) as ReactorAction, + ({ + type: "execute_tools", + calls, + parallel, + addToHistory, + }) as ReactorAction, suspend: (gate) => ({ type: "suspend", gate }) as ReactorAction, fork: (mode, forkId) => ({ type: "fork", mode, forkId }) as ReactorAction, - emit: (eventType, data) => ({ type: "emit", eventType, data }) as ReactorAction, + emit: (eventType, data) => + ({ type: "emit", eventType, data }) as ReactorAction, reply: (content) => ({ type: "reply", content }) as ReactorAction, - checkpoint: (message = "") => ({ type: "checkpoint", message }) as ReactorAction, - compact: (compactor, reason) => ({ type: "compact", compactor, reason }) as ReactorAction, + checkpoint: (message = "") => + ({ type: "checkpoint", message }) as ReactorAction, + compact: (compactor, reason) => + ({ type: "compact", compactor, reason }) as ReactorAction, wait: () => ({ type: "wait" }) as ReactorAction, done: () => ({ type: "done" }) as ReactorAction, }; @@ -36,7 +47,14 @@ function toolOnlyTurn(id: string): ReactorInboundEvent { role: "assistant", model: "test", timestamp: 0, - content: [{ type: "tool_call", id, name: "read_file", arguments: { path: `${id}.ts` } }], + content: [ + { + type: "tool_call", + id, + name: "read_file", + arguments: { path: `${id}.ts` }, + }, + ], }, usage: { input: 0, output: 0 }, source: "test", @@ -50,14 +68,17 @@ function toolDoneEvent(callId: string): ReactorInboundEvent { } as unknown as ReactorInboundEvent; } -function actionsArray(result: ReactorAction | ReactorAction[]): ReactorAction[] { +function actionsArray( + result: ReactorAction | ReactorAction[], +): ReactorAction[] { return Array.isArray(result) ? result : [result]; } function ephemeralText(action: ReactorAction | undefined): string | undefined { if (action === undefined || action.type !== "infer") return undefined; const opts = action.options as - { ephemeralTurns?: { content: { text?: string }[] }[] } | undefined; + | { ephemeralTurns?: { content: { text?: string }[] }[] } + | undefined; return opts?.ephemeralTurns?.[0]?.content?.[0]?.text; } @@ -72,7 +93,9 @@ async function runToolOnlyStreak( for (let i = 0; i < count; i++) { const id = `tc-${i}`; await director.decide(makeTurn(id), mockState, capabilities); - last = actionsArray(await director.decide(toolDoneEvent(id), mockState, capabilities)); + last = actionsArray( + await director.decide(toolDoneEvent(id), mockState, capabilities), + ); } return last; } @@ -82,7 +105,7 @@ describe("ChatDirector tool-only loop protection", () => { test("nudges once at the family threshold, after pending tools execute", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); @@ -96,13 +119,15 @@ describe("ChatDirector tool-only loop protection", () => { test("the nudge is one-shot — it does not repeat on the next tool-only turn", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); await runToolOnlyStreak(director, capabilities, 25); - const nextTurn = actionsArray(await runToolOnlyStreak(director, capabilities, 1)); + const nextTurn = actionsArray( + await runToolOnlyStreak(director, capabilities, 1), + ); const infer = nextTurn.find((a) => a.type === "infer"); expect(infer).toBeDefined(); expect(ephemeralText(infer)).toBeUndefined(); @@ -113,15 +138,17 @@ describe("ChatDirector tool-only loop protection", () => { // well past any prior hard-pause threshold without ever pausing. test("a long productive tool-only streak continues without pausing", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); const actions = await runToolOnlyStreak(director, capabilities, 50); - expect(actions.some((a) => a.type === "reply" && a.content.includes("Auto-paused"))).toBe( - false, - ); + expect( + actions.some( + (a) => a.type === "reply" && a.content.includes("Auto-paused"), + ), + ).toBe(false); expect(actions.some((a) => a.type === "infer")).toBe(true); }); }); @@ -153,13 +180,17 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { "does not re-issue inference for a %s error already exhausted by the harness", async (category) => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); const actions = actionsArray( - await director.decide(inferenceErrorEvent(category), mockState, capabilities), + await director.decide( + inferenceErrorEvent(category), + mockState, + capabilities, + ), ); // No additional full-context send: the base director's terminal @@ -171,29 +202,37 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { test("still recovers on internal-recovery abort, bounded by MAX_INFERENCE_RECOVERIES", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); - const internalAbort = inferenceErrorEvent("aborted", { origin: "internal-recovery" }); + const internalAbort = inferenceErrorEvent("aborted", { + origin: "internal-recovery", + }); // Recovery 1 of 2: re-issues inference. - const first = actionsArray(await director.decide(internalAbort, mockState, capabilities)); + const first = actionsArray( + await director.decide(internalAbort, mockState, capabilities), + ); expect(first.some((a) => a.type === "infer")).toBe(true); // Recovery 2 of 2: re-issues inference. - const second = actionsArray(await director.decide(internalAbort, mockState, capabilities)); + const second = actionsArray( + await director.decide(internalAbort, mockState, capabilities), + ); expect(second.some((a) => a.type === "infer")).toBe(true); // Budget exhausted: no further infer, terminal reply instead. - const third = actionsArray(await director.decide(internalAbort, mockState, capabilities)); + const third = actionsArray( + await director.decide(internalAbort, mockState, capabilities), + ); expect(third.some((a) => a.type === "infer")).toBe(false); expect(third.some((a) => a.type === "reply")).toBe(true); }); test("an unrelated aborted error (not internal-recovery) is not recovered by the director", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); @@ -210,20 +249,28 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { test("inference-recovery budget resets at the next turn boundary", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); - const internalAbort = inferenceErrorEvent("aborted", { origin: "internal-recovery" }); + const internalAbort = inferenceErrorEvent("aborted", { + origin: "internal-recovery", + }); await director.decide(internalAbort, mockState, capabilities); await director.decide(internalAbort, mockState, capabilities); // Budget exhausted for this turn. - const exhausted = actionsArray(await director.decide(internalAbort, mockState, capabilities)); + const exhausted = actionsArray( + await director.decide(internalAbort, mockState, capabilities), + ); expect(exhausted.some((a) => a.type === "infer")).toBe(false); // A fresh turn boundary (inference.done) resets the budget. - await director.decide(toolOnlyTurn("post-boundary"), mockState, capabilities); + await director.decide( + toolOnlyTurn("post-boundary"), + mockState, + capabilities, + ); const afterBoundary = actionsArray( await director.decide(internalAbort, mockState, capabilities), ); @@ -246,15 +293,19 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { // bounded, not open-ended, and never reaches 9. test("worst case: director-owned recovery path issues at most 1 + MAX_INFERENCE_RECOVERIES infer calls", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); - const internalAbort = inferenceErrorEvent("aborted", { origin: "internal-recovery" }); + const internalAbort = inferenceErrorEvent("aborted", { + origin: "internal-recovery", + }); let inferCount = 0; for (let i = 0; i < 10; i++) { - const actions = actionsArray(await director.decide(internalAbort, mockState, capabilities)); + const actions = actionsArray( + await director.decide(internalAbort, mockState, capabilities), + ); if (actions.some((a) => a.type === "infer")) inferCount++; else break; } @@ -263,17 +314,25 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { test("timeout category produces the timeout preamble, not the fatal fallback", async () => { const director = createChatDirector("system", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: providerlessPolicy, }); const capabilities = makeCapabilities(); const actions = actionsArray( - await director.decide(inferenceErrorEvent("timeout"), mockState, capabilities), + await director.decide( + inferenceErrorEvent("timeout"), + mockState, + capabilities, + ), ); const reply = actions.find((a) => a.type === "reply"); expect(reply).toBeDefined(); - expect((reply as { content: string }).content).toContain("did not respond in time"); - expect((reply as { content: string }).content).not.toContain("unrecoverable inference error"); + expect((reply as { content: string }).content).toContain( + "did not respond in time", + ); + expect((reply as { content: string }).content).not.toContain( + "unrecoverable inference error", + ); }); }); diff --git a/src/agent/director.ts b/src/agent/director.ts index 81e07b5fa..a9f653a4a 100644 --- a/src/agent/director.ts +++ b/src/agent/director.ts @@ -1,4 +1,7 @@ -import { DefaultDirector, type ExtendedInferenceOptions } from "@intx/inference"; +import { + DefaultDirector, + type ExtendedInferenceOptions, +} from "@intx/inference"; import { getLogger } from "@intx/log"; import type { ReactorDirector, @@ -16,14 +19,25 @@ import { isCompactSpacerEchoTurn, } from "../session/compactor.js"; import type { WorkflowCoordinator } from "../workflows/coordinator.js"; -import { createCompactionGovernor, type CompactionGovernor } from "./compaction.js"; +import { + createCompactionGovernor, + type CompactionGovernor, +} from "./compaction.js"; import { onTurnBoundary } from "./reactor-events.js"; import { type } from "arktype"; -import { applyManageTasks, hasActiveTasks, parseManageTasksArgs, type Task } from "./tasks.js"; +import { + applyManageTasks, + hasActiveTasks, + parseManageTasksArgs, + type Task, +} from "./tasks.js"; import { createCorbitsRetryPolicy } from "./retry-policy.js"; import { isInternalRecoveryAbortRaw } from "../inference-abort.js"; import { LOG_NAMESPACE_ROOT } from "../branding.js"; -import { resolveModelFamilyPolicy, type ModelFamilyPolicy } from "./model-family-policy.js"; +import { + resolveModelFamilyPolicy, + type ModelFamilyPolicy, +} from "./model-family-policy.js"; import { PRESENT_VIEW_PRIMITIVES_GUIDANCE } from "./tool-schema-normalize.js"; import { APPROVER_REJECTION_MARKER, @@ -89,7 +103,12 @@ function ensureCycleSettlesWithReply( ): ReactorAction | ReactorAction[] { const list = Array.isArray(actions) ? actions : [actions]; if (list.at(-1)?.type !== "wait") return actions; - if (list.some((a) => a.type === "infer" || a.type === "execute_tools" || a.type === "reply")) { + if ( + list.some( + (a) => + a.type === "infer" || a.type === "execute_tools" || a.type === "reply", + ) + ) { return actions; } return [...list.slice(0, -1), capabilities.reply("")]; @@ -189,7 +208,14 @@ export const presentDefinition: ToolDefinition = { text: { type: "string" }, tone: { type: "string", - enum: ["default", "muted", "success", "warning", "danger", "accent"], + enum: [ + "default", + "muted", + "success", + "warning", + "danger", + "accent", + ], }, bold: { type: "boolean" }, dim: { type: "boolean" }, @@ -242,7 +268,9 @@ export const presentDefinition: ToolDefinition = { type: "array", items: { type: "object", - properties: { align: { type: "string", enum: ["left", "right", "center"] } }, + properties: { + align: { type: "string", enum: ["left", "right", "center"] }, + }, additionalProperties: false, }, }, @@ -295,9 +323,14 @@ export const submitOutputDefinition: ToolDefinition = { // either carries a reason the model should respond to or doesn't (canned // reply stands). The marker strings themselves live in // permission/decline-markers.ts alongside their producing seams. -type DeclinedToolResult = { kind: "approver-rejection"; reason?: string } | { kind: "policy-deny" }; +type DeclinedToolResult = + | { kind: "approver-rejection"; reason?: string } + | { kind: "policy-deny" }; -const POLICY_DENY_MARKERS = [DENIED_BY_POLICY_MARKER, NO_MATCHING_GRANTS_MARKER] as const; +const POLICY_DENY_MARKERS = [ + DENIED_BY_POLICY_MARKER, + NO_MATCHING_GRANTS_MARKER, +] as const; function isPolicyDeny(content: string): boolean { return POLICY_DENY_MARKERS.some((marker) => content.includes(marker)); @@ -307,7 +340,9 @@ function isPolicyDeny(content: string): boolean { // reactor path after "denied by approver: ". Both are undefined when the // operator declined without a reason. function approverRejectionReason(content: string): string | undefined { - const reactor = content.match(new RegExp(`${APPROVER_REJECTION_MARKER}: (.+)`)); + const reactor = content.match( + new RegExp(`${APPROVER_REJECTION_MARKER}: (.+)`), + ); if (reactor !== null) return reactor[1]; if (content.includes(OPERATOR_DECLINED_MARKER)) { const separator = content.indexOf(" — "); @@ -320,10 +355,14 @@ function classifyDeclinedToolResult(result: { content: unknown; isError?: boolean; }): DeclinedToolResult | null { - if (result.isError !== true || typeof result.content !== "string") return null; + if (result.isError !== true || typeof result.content !== "string") + return null; const content = result.content; if (isPolicyDeny(content)) return { kind: "policy-deny" }; - if (content.includes(APPROVER_REJECTION_MARKER) || content.includes(OPERATOR_DECLINED_MARKER)) { + if ( + content.includes(APPROVER_REJECTION_MARKER) || + content.includes(OPERATOR_DECLINED_MARKER) + ) { const reason = approverRejectionReason(content); return reason === undefined ? { kind: "approver-rejection" } @@ -361,7 +400,8 @@ function applyManageTasksToolCall( export interface ChatDirectorOptions { taskClassifier?: - ((message: string, metadata: SessionMetadata) => Promise) | undefined; + | ((message: string, metadata: SessionMetadata) => Promise) + | undefined; onActivateTools?: ((names: string[]) => void) | undefined; inactivityTimeoutMs?: number | undefined; totalTimeoutMs?: number | undefined; @@ -394,12 +434,16 @@ type ChatDirectorImplOptions = Omit & { }; class ChatDirectorImpl extends DefaultDirector { - private readonly workflowCalls = new Map(); + private readonly workflowCalls = new Map< + string, + { name: string; args: unknown } + >(); private readonly lspTriggerCalls = new Set(); private readonly askOperatorCalls = new Set(); private readonly onActivateTools: ((names: string[]) => void) | undefined; private readonly taskClassifier: - ((message: string, metadata: SessionMetadata) => Promise) | undefined; + | ((message: string, metadata: SessionMetadata) => Promise) + | undefined; private readonly _systemPrompt: string; private _toolDefinitions: ToolDefinition[]; private inactivityTimeoutMs: number | undefined; @@ -453,7 +497,8 @@ class ChatDirectorImpl extends DefaultDirector { toolDefinitions, ); this.modelFamilyPolicy = - options.modelFamilyPolicy ?? resolveModelFamilyPolicy({ providerName: "" }); + options.modelFamilyPolicy ?? + resolveModelFamilyPolicy({ providerName: "" }); this.retryPolicy = options.retryPolicy ?? createCorbitsRetryPolicy(); this.getLiveFleetCount = options.getLiveFleetCount; } @@ -483,18 +528,26 @@ class ChatDirectorImpl extends DefaultDirector { // or zeroes usage on the latest turn — a local lower-then-corrected bound // beats displaying a number the provider never actually reported. getContextEstimate(): { tokens: number; isEstimate: boolean } { - return { tokens: this.compaction.estimatedTokens, isEstimate: this.compaction.usingEstimate }; + return { + tokens: this.compaction.estimatedTokens, + isEstimate: this.compaction.usingEstimate, + }; } private openTaskIds(): string[] { - return this.tasks.filter((t) => t.status === "todo" || t.status === "doing").map((t) => t.id); + return this.tasks + .filter((t) => t.status === "todo" || t.status === "doing") + .map((t) => t.id); } private logTerminationWithOpenTasks(path: string): void { - logger.error("Director reached a terminal decision on {path} with open tasks: {openTasks}", { - path, - openTasks: this.openTaskIds(), - }); + logger.error( + "Director reached a terminal decision on {path} with open tasks: {openTasks}", + { + path, + openTasks: this.openTaskIds(), + }, + ); } /** @@ -515,7 +568,10 @@ class ChatDirectorImpl extends DefaultDirector { this.pendingToolOnlyNudge = false; const rewritten = [...actions]; - const existing = actions[inferIndex] as Extract; + const existing = actions[inferIndex] as Extract< + ReactorAction, + { type: "infer" } + >; rewritten[inferIndex] = inferWithNudge( capabilities, this.modelFamilyPolicy.wrapUpNudgeText, @@ -532,11 +588,15 @@ class ChatDirectorImpl extends DefaultDirector { // activating a workflow never grows the tools array and busts the cache // prefix. Outside a workflow it is a harmless no-op the director ignores // unless the call is a terminal task submission. - const tools = this._toolDefinitions.some((t) => t.name === submitOutputDefinition.name) + const tools = this._toolDefinitions.some( + (t) => t.name === submitOutputDefinition.name, + ) ? this._toolDefinitions : [...this._toolDefinitions, submitOutputDefinition]; - const directive = active ? (this.workflowCoordinator?.directive() ?? null) : null; + const directive = active + ? (this.workflowCoordinator?.directive() ?? null) + : null; const rewrite = (action: ReactorAction): ReactorAction => { if (action.type !== "infer") return action; @@ -548,7 +608,8 @@ class ChatDirectorImpl extends DefaultDirector { }; if (this.inactivityTimeoutMs !== undefined) options.inactivityTimeoutMs = this.inactivityTimeoutMs; - if (this.totalTimeoutMs !== undefined) options.totalTimeoutMs = this.totalTimeoutMs; + if (this.totalTimeoutMs !== undefined) + options.totalTimeoutMs = this.totalTimeoutMs; if (directive !== null) { return { type: "infer", @@ -587,7 +648,10 @@ class ChatDirectorImpl extends DefaultDirector { if (afterCompact === "meter") return capabilities.wait(); return capabilities.infer(); } - const idleCompact = this.compaction.interceptIdleContinuation(event, capabilities); + const idleCompact = this.compaction.interceptIdleContinuation( + event, + capabilities, + ); if (idleCompact !== null) return idleCompact; const recovery = this.compaction.interceptOverflow(event, capabilities); if (recovery !== null) return recovery; @@ -613,12 +677,17 @@ class ChatDirectorImpl extends DefaultDirector { if (this.inferenceRecoveries < MAX_INFERENCE_RECOVERIES) { this.inferenceRecoveries++; logger.warn`inference-recovery attempt=${String(this.inferenceRecoveries)} max=${String(MAX_INFERENCE_RECOVERIES)} category=${event.error.category}`; - return [capabilities.checkpoint("inference-recovery"), capabilities.infer()]; + return [ + capabilities.checkpoint("inference-recovery"), + capabilities.infer(), + ]; } logger.warn`inference-recovery-exhausted max=${String(MAX_INFERENCE_RECOVERIES)} category=${event.error.category}`; return [ capabilities.checkpoint("inference-recovery-exhausted"), - capabilities.reply("The request could not recover. Send a message to resume."), + capabilities.reply( + "The request could not recover. Send a message to resume.", + ), ]; } @@ -632,7 +701,10 @@ class ChatDirectorImpl extends DefaultDirector { // as a terminal reply, so the misleading "unrecoverable" wording would // become the routine message for an ordinary timeout. Intercept it here // with accurate, calm wording rather than patching the vendored map. - if (event.type === "inference.error" && event.error.category === "timeout") { + if ( + event.type === "inference.error" && + event.error.category === "timeout" + ) { return [ capabilities.checkpoint("inference-error"), capabilities.reply( @@ -658,9 +730,13 @@ class ChatDirectorImpl extends DefaultDirector { } if (onTurnBoundary(event)) this.inferenceRecoveries = 0; - if (event.type === "message.received" && this.taskClassifier !== undefined) { + if ( + event.type === "message.received" && + this.taskClassifier !== undefined + ) { const message = event.message; - const content = typeof message.content === "string" ? message.content : ""; + const content = + typeof message.content === "string" ? message.content : ""; const metadata: SessionMetadata = { turnCount: this.turnCount, currentTaskLabel: this.currentTaskLabel, @@ -700,10 +776,15 @@ class ChatDirectorImpl extends DefaultDirector { if (onTurnBoundary(event)) { this.turnCount++; - const hasToolCalls = event.turn.content.some((b) => b.type === "tool_call"); + const hasToolCalls = event.turn.content.some( + (b) => b.type === "tool_call", + ); const hasText = event.turn.content.some( - (b) => b.type === "text" && typeof b.text === "string" && b.text.length > 0, + (b) => + b.type === "text" && + typeof b.text === "string" && + b.text.length > 0, ) && !isCompactSpacerEchoTurn(event.turn); this.lastInferenceTurnHadContent = hasToolCalls || hasText; @@ -734,7 +815,8 @@ class ChatDirectorImpl extends DefaultDirector { // Count them only after the echo budget is spent so the step-nudge // rail still has its three turns before the stuck reply. const spacerEchoStillNudging = - isCompactSpacerEchoTurn(event.turn) && this.spacerEchoNudges < MAX_SPACER_ECHO_NUDGES; + isCompactSpacerEchoTurn(event.turn) && + this.spacerEchoNudges < MAX_SPACER_ECHO_NUDGES; if (!spacerEchoStillNudging) this.workflowIdleTurns++; } } @@ -752,7 +834,10 @@ class ChatDirectorImpl extends DefaultDirector { if (isCodeFile(path)) this.lspTriggerCalls.add(block.id); } if (block.name === "submit_output") { - this.workflowCalls.set(block.id, { name: block.name, args: block.arguments }); + this.workflowCalls.set(block.id, { + name: block.name, + args: block.arguments, + }); } if (block.name === "ask_operator") { this.askOperatorCalls.add(block.id); @@ -760,7 +845,10 @@ class ChatDirectorImpl extends DefaultDirector { } } - if (event.type === "tool.done" && this.workflowCalls.has(event.result.callId)) { + if ( + event.type === "tool.done" && + this.workflowCalls.has(event.result.callId) + ) { const call = this.workflowCalls.get(event.result.callId); this.workflowCalls.delete(event.result.callId); const advanced = this.workflowCoordinator?.handleToolDone( @@ -771,14 +859,20 @@ class ChatDirectorImpl extends DefaultDirector { if (advanced) this.workflowIdleTurns = 0; } - if (event.type === "tool.done" && this.askOperatorCalls.has(event.result.callId)) { + if ( + event.type === "tool.done" && + this.askOperatorCalls.has(event.result.callId) + ) { this.askOperatorCalls.delete(event.result.callId); if (!event.result.isError) { this.operatorJustResponded = true; } } - if (event.type === "tool.done" && this.lspTriggerCalls.has(event.result.callId)) { + if ( + event.type === "tool.done" && + this.lspTriggerCalls.has(event.result.callId) + ) { this.lspTriggerCalls.delete(event.result.callId); if (!event.result.isError) this.onActivateTools?.(["lsp"]); } @@ -820,7 +914,10 @@ class ChatDirectorImpl extends DefaultDirector { this.compaction.noteInferenceDone(event, turns); } - if (event.type === "inference.done" && isCompactSpacerEchoTurn(event.turn)) { + if ( + event.type === "inference.done" && + isCompactSpacerEchoTurn(event.turn) + ) { if (this.spacerEchoNudges < MAX_SPACER_ECHO_NUDGES) { this.spacerEchoNudges++; return inferWithNudge(capabilities, SPACER_ECHO_NUDGE); @@ -832,11 +929,17 @@ class ChatDirectorImpl extends DefaultDirector { const spacerEchoExhausted = event.type === "inference.done" && isCompactSpacerEchoTurn(event.turn); if (spacerEchoExhausted) { - baseActions = baseActions.map((a) => (a.type === "reply" ? capabilities.wait() : a)); + baseActions = baseActions.map((a) => + a.type === "reply" ? capabilities.wait() : a, + ); } this.compaction.noteIdleTurn(event, baseActions); - const compacted = this.compaction.interceptActions(event, baseActions, capabilities); + const compacted = this.compaction.interceptActions( + event, + baseActions, + capabilities, + ); if (compacted !== null) return compacted; // Loop protection takes precedence over workflow/open-task @@ -848,19 +951,28 @@ class ChatDirectorImpl extends DefaultDirector { // rewrites an `infer` action once pending tools have resolved and one // is actually present in the batch (mirrors the sub-agent report-forced // wiring in src/subagent/index.ts). - const toolOnlyRewrite = this.applyToolOnlyLoopProtection(baseActions, capabilities); + const toolOnlyRewrite = this.applyToolOnlyLoopProtection( + baseActions, + capabilities, + ); if (toolOnlyRewrite !== null) return toolOnlyRewrite; const coordinator = this.workflowCoordinator; if (coordinator?.isActive() && !coordinator.currentStepIsGate()) { - const hasTerminal = baseActions.some((a) => a.type === "wait" || a.type === "reply"); - if (hasTerminal && (this.lastInferenceTurnHadContent || spacerEchoExhausted)) { + const hasTerminal = baseActions.some( + (a) => a.type === "wait" || a.type === "reply", + ); + if ( + hasTerminal && + (this.lastInferenceTurnHadContent || spacerEchoExhausted) + ) { if (this.operatorJustResponded) { this.operatorJustResponded = false; return baseActions; } if (this.workflowIdleTurns >= 3) { - if (hasActiveTasks(this.tasks)) this.logTerminationWithOpenTasks("workflow-idle-stall"); + if (hasActiveTasks(this.tasks)) + this.logTerminationWithOpenTasks("workflow-idle-stall"); return [ capabilities.reply( "The workflow appears stuck on this step. Send a message to continue or advance manually.", @@ -877,8 +989,12 @@ class ChatDirectorImpl extends DefaultDirector { `If this step is complete, ${stepClause}. ` + `Otherwise continue working with tools.`; const passThrough = baseActions.filter( - (a): a is Exclude => - a.type !== "wait" && a.type !== "reply", + ( + a, + ): a is Exclude< + ReactorAction, + { type: "wait" } | { type: "reply" } + > => a.type !== "wait" && a.type !== "reply", ); return [...passThrough, inferWithNudge(capabilities, nudge)]; } @@ -887,9 +1003,12 @@ class ChatDirectorImpl extends DefaultDirector { // A workflow gate step is a legitimate pause for operator approval, so // yielding there with open tasks is not an invariant breach — leave it to // the workflow runtime and do not nudge. - const atWorkflowGate = coordinator?.isActive() === true && coordinator.currentStepIsGate(); + const atWorkflowGate = + coordinator?.isActive() === true && coordinator.currentStepIsGate(); if (!atWorkflowGate && hasActiveTasks(this.tasks)) { - const hasTerminal = baseActions.some((a) => a.type === "wait" || a.type === "reply"); + const hasTerminal = baseActions.some( + (a) => a.type === "wait" || a.type === "reply", + ); if (hasTerminal) { if ((this.getLiveFleetCount?.() ?? 0) > 0) { return base; @@ -897,14 +1016,20 @@ class ChatDirectorImpl extends DefaultDirector { if (this.idleTerminationNudges < MAX_OPEN_TASK_NUDGES) { this.idleTerminationNudges++; const passThrough = baseActions.filter( - (a): a is Exclude => - a.type !== "wait" && a.type !== "reply", + ( + a, + ): a is Exclude< + ReactorAction, + { type: "wait" } | { type: "reply" } + > => a.type !== "wait" && a.type !== "reply", ); // Inside a workflow the terminal action is submit_output with the // current step id, so point the nudge at it rather than the general // manage_tasks guidance. const nudge = - coordinator?.isActive() === true ? WORKFLOW_OPEN_TASK_NUDGE : IDLE_OPEN_TASK_NUDGE; + coordinator?.isActive() === true + ? WORKFLOW_OPEN_TASK_NUDGE + : IDLE_OPEN_TASK_NUDGE; return [...passThrough, inferWithNudge(capabilities, nudge)]; } this.logTerminationWithOpenTasks("idle-stall"); @@ -925,7 +1050,8 @@ export function createChatDirector( ...rest, // `provider` is raw {providerName, model} input; the constructor wants // the resolved ModelFamilyPolicy, not the input it was resolved from. - modelFamilyPolicy: provider !== undefined ? resolveModelFamilyPolicy(provider) : undefined, + modelFamilyPolicy: + provider !== undefined ? resolveModelFamilyPolicy(provider) : undefined, // Stamp provider id onto retry errors so known-xAI short 429s remap. // Prefer an explicit policy, then a live getter (mid-session `/model`), // then the bootstrap providerName. diff --git a/src/agent/directors/bake-skills.test.ts b/src/agent/directors/bake-skills.test.ts index feeca52f1..edf26119d 100644 --- a/src/agent/directors/bake-skills.test.ts +++ b/src/agent/directors/bake-skills.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { formatBakedOptionalSkills, loadBakedSkillBody } from "./bake-skills.js"; +import { + formatBakedOptionalSkills, + loadBakedSkillBody, +} from "./bake-skills.js"; function stripFrontmatter(raw: string): string { if (!raw.startsWith("---")) return raw.trim(); @@ -12,25 +15,37 @@ function stripFrontmatter(raw: string): string { const styleOnDisk = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/style/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/style/SKILL.md", + ), "utf8", ), ); const philosophyOnDisk = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/philosophy/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/philosophy/SKILL.md", + ), "utf8", ), ); const ponytailOnDisk = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/ponytail/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/ponytail/SKILL.md", + ), "utf8", ), ); const nativeRuntimeOnDisk = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/native-runtime/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/native-runtime/SKILL.md", + ), "utf8", ), ); diff --git a/src/agent/directors/bake-skills.ts b/src/agent/directors/bake-skills.ts index f08e49f25..7e6b1a3a9 100644 --- a/src/agent/directors/bake-skills.ts +++ b/src/agent/directors/bake-skills.ts @@ -28,7 +28,9 @@ function skillsRootCandidates(): string[] { out.push(join(here, "plugins", "corbits-skills", "skills")); // Compiled binary: plugins next to execPath if (process.execPath.length > 0) { - out.push(join(dirname(process.execPath), "plugins", "corbits-skills", "skills")); + out.push( + join(dirname(process.execPath), "plugins", "corbits-skills", "skills"), + ); } return out; } diff --git a/src/agent/directors/bruckheimer/package.test.ts b/src/agent/directors/bruckheimer/package.test.ts index 6ade3314a..b3f896381 100644 --- a/src/agent/directors/bruckheimer/package.test.ts +++ b/src/agent/directors/bruckheimer/package.test.ts @@ -8,7 +8,9 @@ describe("bruckheimerPackage", () => { test("systemPrompt is real (not Placeholder)", () => { expect(bruckheimerPackage.systemPrompt.length).toBeGreaterThan(0); - expect(bruckheimerPackage.systemPrompt.startsWith("Placeholder")).toBe(false); + expect(bruckheimerPackage.systemPrompt.startsWith("Placeholder")).toBe( + false, + ); }); test("systemPrompt states PRIMARY INTENT", () => { @@ -122,7 +124,11 @@ describe("bruckheimerPackage", () => { expect(bruckheimerPackage.primaryIntent).toMatch(/product discovery/i); expect(bruckheimerPackage.outOfLane).toContain("shipping product code"); expect(bruckheimerPackage.outOfLane).toContain("architecture gates"); - expect(bruckheimerPackage.outOfLane).toContain("ongoing P/A/I docs maintenance as Shakespeare"); - expect(bruckheimerPackage.outOfLane).toContain("ordered eng plans as Counsel"); + expect(bruckheimerPackage.outOfLane).toContain( + "ongoing P/A/I docs maintenance as Shakespeare", + ); + expect(bruckheimerPackage.outOfLane).toContain( + "ordered eng plans as Counsel", + ); }); }); diff --git a/src/agent/directors/bruckheimer/package.ts b/src/agent/directors/bruckheimer/package.ts index 795a65374..688956342 100644 --- a/src/agent/directors/bruckheimer/package.ts +++ b/src/agent/directors/bruckheimer/package.ts @@ -8,7 +8,8 @@ import { DOCS_TOOLS } from "../tool-sets.js"; */ export const bruckheimerPackage: DirectorPackage = { id: "bruckheimer", - primaryIntent: "Product discovery docs — invent/capture product shape; do not implement", + primaryIntent: + "Product discovery docs — invent/capture product shape; do not implement", outOfLane: [ "shipping product code", "architecture gates", @@ -18,7 +19,8 @@ export const bruckheimerPackage: DirectorPackage = { "ongoing P/A/I docs maintenance as Shakespeare", "ordered eng plans as Counsel", ], - description: "Product discovery specialist — user/product shape docs, not code", + description: + "Product discovery specialist — user/product shape docs, not code", tools: { allow: DOCS_TOOLS }, spawn: { maySpawn: false }, tier: "leaf", diff --git a/src/agent/directors/builder/package.test.ts b/src/agent/directors/builder/package.test.ts index 386a271d4..3d939be01 100644 --- a/src/agent/directors/builder/package.test.ts +++ b/src/agent/directors/builder/package.test.ts @@ -41,8 +41,12 @@ describe("builderPackage", () => { expect(p).toMatch(/Don't shortcut verify/i); expect(p).toMatch(/partial gates/i); expect(p).toMatch(/pre-existing/i); - expect(p).toMatch(/defined typecheck command.*relevant tests.*defined full check/is); - expect(p).toMatch(/repository defines no typecheck command.*explicit Blocker/is); + expect(p).toMatch( + /defined typecheck command.*relevant tests.*defined full check/is, + ); + expect(p).toMatch( + /repository defines no typecheck command.*explicit Blocker/is, + ); expect(p).toMatch(/evidence.*AGENTS.*package scripts/is); expect(p).toMatch(/do not invent.*typecheck command/i); expect(p).toMatch(/exact verification command.*outcome.*exit status/is); @@ -63,7 +67,9 @@ describe("builderPackage", () => { test("systemPrompt requires baked core constraints and Ponytail prerequisites", () => { const p = builderPackage.systemPrompt; expect(p).toContain("Prerequisites"); - expect(p).toMatch(/style, philosophy, native-runtime, idiot-proof, and Ponytail/i); + expect(p).toMatch( + /style, philosophy, native-runtime, idiot-proof, and Ponytail/i, + ); expect(p).toMatch(/use_skill is not mounted/i); expect(p).toMatch( /including their TypeScript conventions when TypeScript is the task surface/i, diff --git a/src/agent/directors/builder/package.ts b/src/agent/directors/builder/package.ts index 149dee20b..f4649bc06 100644 --- a/src/agent/directors/builder/package.ts +++ b/src/agent/directors/builder/package.ts @@ -8,7 +8,8 @@ import { BUILD_TOOLS } from "../tool-sets.js"; */ export const builderPackage: DirectorPackage = { id: "builder", - primaryIntent: "Implement the brief in product code — edit, verify, report; nothing more", + primaryIntent: + "Implement the brief in product code — edit, verify, report; nothing more", outOfLane: [ "inventing architecture beyond the brief", "expanding scope after success criteria are met", @@ -18,7 +19,13 @@ export const builderPackage: DirectorPackage = { "orchestrating or spawning other agents", ], description: "Implementation worker — edit, verify, report", - optionalSkills: ["style", "philosophy", "native-runtime", "idiot-proof", "ponytail"], + optionalSkills: [ + "style", + "philosophy", + "native-runtime", + "idiot-proof", + "ponytail", + ], tools: { allow: BUILD_TOOLS }, spawn: { maySpawn: false }, tier: "leaf", diff --git a/src/agent/directors/counsel/package.test.ts b/src/agent/directors/counsel/package.test.ts index 07cc64e3a..2dd0a0ac9 100644 --- a/src/agent/directors/counsel/package.test.ts +++ b/src/agent/directors/counsel/package.test.ts @@ -79,19 +79,29 @@ describe("counselPackage", () => { }); test("optionalSkills order", () => { - expect(counselPackage.optionalSkills).toEqual(["style", "philosophy", "native-integration"]); + expect(counselPackage.optionalSkills).toEqual([ + "style", + "philosophy", + "native-integration", + ]); }); test("does not advertise interview skill workers cannot use", () => { expect(counselPackage.optionalSkills).not.toContain("interview"); - expect(counselPackage.systemPrompt).not.toMatch(/interview-skill awareness/i); + expect(counselPackage.systemPrompt).not.toMatch( + /interview-skill awareness/i, + ); }); test("primaryIntent and outOfLane match counsel / plan lane", () => { - expect(counselPackage.primaryIntent).toBe("Author ordered eng change plans; do not implement"); + expect(counselPackage.primaryIntent).toBe( + "Author ordered eng change plans; do not implement", + ); expect(counselPackage.description).toMatch(/Counsel/i); expect(counselPackage.outOfLane).toContain("shipping code"); - expect(counselPackage.outOfLane).toContain("architecture gate sign-off as Greybeard"); + expect(counselPackage.outOfLane).toContain( + "architecture gate sign-off as Greybeard", + ); expect(counselPackage.outOfLane).toContain("running the fleet"); expect(counselPackage.outOfLane).toContain("pure code review"); expect(counselPackage.outOfLane).toContain("becoming Builder or Critic"); diff --git a/src/agent/directors/critic/package.test.ts b/src/agent/directors/critic/package.test.ts index 24a03446a..c36052d40 100644 --- a/src/agent/directors/critic/package.test.ts +++ b/src/agent/directors/critic/package.test.ts @@ -43,7 +43,9 @@ describe("criticPackage", () => { test("systemPrompt is correctness plus this-diff hygiene", () => { const p = criticPackage.systemPrompt; expect(p).toMatch(/Correctness and this-diff hygiene/i); - expect(p).toMatch(/correctness or the stated requirements\/success_criteria/i); + expect(p).toMatch( + /correctness or the stated requirements\/success_criteria/i, + ); expect(p).toMatch(/hygiene this diff introduced/i); expect(p).toMatch(/dead code/i); expect(p).toMatch(/file-for-later/i); @@ -54,15 +56,21 @@ describe("criticPackage", () => { test("systemPrompt flags API contract / sync→async as blocking", () => { expect(criticPackage.systemPrompt).toMatch(/API contract check/i); - expect(criticPackage.systemPrompt).toMatch(/blocking when brief specifies signatures/i); + expect(criticPackage.systemPrompt).toMatch( + /blocking when brief specifies signatures/i, + ); expect(criticPackage.systemPrompt).toMatch(/public exports/i); expect(criticPackage.systemPrompt).toMatch(/Sync\s*→\s*async/i); expect(criticPackage.systemPrompt).toMatch( /returning Promise when callers expect a plain value/i, ); expect(criticPackage.systemPrompt).toMatch(/blocking correctness defect/i); - expect(criticPackage.systemPrompt).toMatch(/parameter order\/optionality\/return-type drift/i); - expect(criticPackage.systemPrompt).toMatch(/Rank these as blocking, not style nits/i); + expect(criticPackage.systemPrompt).toMatch( + /parameter order\/optionality\/return-type drift/i, + ); + expect(criticPackage.systemPrompt).toMatch( + /Rank these as blocking, not style nits/i, + ); }); test("systemPrompt has no tool-schema restatement or fake caps", () => { @@ -109,7 +117,9 @@ describe("criticPackage", () => { "Evidence-based code review including hygiene the diff introduced; never fix product code", ); expect(criticPackage.outOfLane).toContain("implementing fixes"); - expect(criticPackage.outOfLane).toContain("architecture portfolio without code evidence"); + expect(criticPackage.outOfLane).toContain( + "architecture portfolio without code evidence", + ); expect(criticPackage.outOfLane).toContain("visual brand"); expect(criticPackage.outOfLane).toContain("DESIGN.md"); expect(criticPackage.outOfLane).toContain("pedantic fun without evidence"); diff --git a/src/agent/directors/emil/package.test.ts b/src/agent/directors/emil/package.test.ts index 9217b590f..43a50fd00 100644 --- a/src/agent/directors/emil/package.test.ts +++ b/src/agent/directors/emil/package.test.ts @@ -92,12 +92,20 @@ describe("emilPackage", () => { expect(emilPackage.primaryIntent).toBe( "Design-engineering laws review; never fix product code", ); - expect(emilPackage.outOfLane).toContain("shipping product code without design brief"); + expect(emilPackage.outOfLane).toContain( + "shipping product code without design brief", + ); expect(emilPackage.outOfLane).toContain("marketing content"); expect(emilPackage.outOfLane).toContain("applying product fixes"); - expect(emilPackage.outOfLane).toContain("suggesting full rewrites as implementer"); - expect(emilPackage.outOfLane).toContain("CBS visual token ownership (draper)"); + expect(emilPackage.outOfLane).toContain( + "suggesting full rewrites as implementer", + ); + expect(emilPackage.outOfLane).toContain( + "CBS visual token ownership (draper)", + ); expect(emilPackage.outOfLane).toContain("DESIGN.md ownership (rand)"); - expect(emilPackage.outOfLane).toContain("correctness-severity ownership (critic)"); + expect(emilPackage.outOfLane).toContain( + "correctness-severity ownership (critic)", + ); }); }); diff --git a/src/agent/directors/gaasbot/package.test.ts b/src/agent/directors/gaasbot/package.test.ts index 75cd61732..df5a018e3 100644 --- a/src/agent/directors/gaasbot/package.test.ts +++ b/src/agent/directors/gaasbot/package.test.ts @@ -80,16 +80,25 @@ describe("gaasbotPackage", () => { }); test("optionalSkills is philosophy and native-integration", () => { - expect(gaasbotPackage.optionalSkills).toEqual(["philosophy", "native-integration"]); + expect(gaasbotPackage.optionalSkills).toEqual([ + "philosophy", + "native-integration", + ]); }); test("primaryIntent and outOfLane match risk counsel lane", () => { expect(gaasbotPackage.primaryIntent).toMatch(/[Rr]isk counsel/i); expect(gaasbotPackage.description).toMatch(/[Rr]isk counsel/i); expect(gaasbotPackage.outOfLane).toContain("blocking merges"); - expect(gaasbotPackage.outOfLane).toContain("shipping product code as implementer"); - expect(gaasbotPackage.outOfLane).toContain("replacing greybeard architecture review"); - expect(gaasbotPackage.outOfLane).toContain("replacing plan eng change plans"); + expect(gaasbotPackage.outOfLane).toContain( + "shipping product code as implementer", + ); + expect(gaasbotPackage.outOfLane).toContain( + "replacing greybeard architecture review", + ); + expect(gaasbotPackage.outOfLane).toContain( + "replacing plan eng change plans", + ); expect(gaasbotPackage.outOfLane).toContain("applying product fixes"); }); }); diff --git a/src/agent/directors/greybeard/package.test.ts b/src/agent/directors/greybeard/package.test.ts index ada3e289f..0077dd38b 100644 --- a/src/agent/directors/greybeard/package.test.ts +++ b/src/agent/directors/greybeard/package.test.ts @@ -81,7 +81,11 @@ describe("greybeardPackage", () => { test("spawn.maySpawn is true with limited allowlist", () => { expect(greybeardPackage.spawn.maySpawn).toBe(true); - expect(greybeardPackage.spawn.allowlist).toEqual(["intern", "explorer", "critic"]); + expect(greybeardPackage.spawn.allowlist).toEqual([ + "intern", + "explorer", + "critic", + ]); }); test("allowlist is only intern, explorer, critic", () => { @@ -113,12 +117,20 @@ describe("greybeardPackage", () => { }); test("optionalSkills order", () => { - expect(greybeardPackage.optionalSkills).toEqual(["style", "philosophy", "native-integration"]); + expect(greybeardPackage.optionalSkills).toEqual([ + "style", + "philosophy", + "native-integration", + ]); }); test("primaryIntent and outOfLane match greybeard lane", () => { - expect(greybeardPackage.primaryIntent).toBe("Architecture judgment; limited spawn"); + expect(greybeardPackage.primaryIntent).toBe( + "Architecture judgment; limited spawn", + ); expect(greybeardPackage.outOfLane).toContain("shipping product code"); - expect(greybeardPackage.outOfLane).toContain("pedantic style-only nitpicking"); + expect(greybeardPackage.outOfLane).toContain( + "pedantic style-only nitpicking", + ); }); }); diff --git a/src/agent/directors/identity.test.ts b/src/agent/directors/identity.test.ts index cc727eb31..553675c71 100644 --- a/src/agent/directors/identity.test.ts +++ b/src/agent/directors/identity.test.ts @@ -21,7 +21,9 @@ describe("formatDirectorSystemPrompt", () => { expect(text.startsWith("Identity: agent id `builder`")).toBe(true); expect(text).toContain('spawn_agent(agent="builder")'); expect(text).toContain("Model role: implement."); - expect(text).toContain("style, philosophy, native-runtime, idiot-proof, ponytail"); + expect(text).toContain( + "style, philosophy, native-runtime, idiot-proof, ponytail", + ); expect(text).toContain(DIRECTOR_REGISTRY.builder.systemPrompt); }); @@ -34,31 +36,46 @@ describe("formatDirectorSystemPrompt", () => { const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.builder); const style = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/style/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/style/SKILL.md", + ), "utf8", ), ); const philosophy = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/philosophy/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/philosophy/SKILL.md", + ), "utf8", ), ); const nativeRuntime = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/native-runtime/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/native-runtime/SKILL.md", + ), "utf8", ), ); const idiotProof = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/idiot-proof/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/idiot-proof/SKILL.md", + ), "utf8", ), ); const ponytail = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/ponytail/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/ponytail/SKILL.md", + ), "utf8", ), ); @@ -73,7 +90,10 @@ describe("formatDirectorSystemPrompt", () => { ); const typescript = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/typescript/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/typescript/SKILL.md", + ), "utf8", ), ); @@ -131,7 +151,10 @@ describe("formatDirectorSystemPrompt", () => { const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.counsel); const interview = stripFrontmatter( readFileSync( - join(import.meta.dirname, "../../../plugins/corbits-skills/skills/interview/SKILL.md"), + join( + import.meta.dirname, + "../../../plugins/corbits-skills/skills/interview/SKILL.md", + ), "utf8", ), ); @@ -143,7 +166,9 @@ describe("formatDirectorSystemPrompt", () => { expect(text).not.toContain(interview); expect(text).not.toContain("### interview"); // interview recipe centers on ask_operator batches; counsel must not embed it - expect(text).not.toMatch(/multiple-choice questions in batches via `ask_operator`/); + expect(text).not.toMatch( + /multiple-choice questions in batches via `ask_operator`/, + ); expect(text).toContain("style, philosophy"); expect(text).toContain("# Baked skill guidance"); }); diff --git a/src/agent/directors/identity.ts b/src/agent/directors/identity.ts index ed0a4da2f..919ff64fe 100644 --- a/src/agent/directors/identity.ts +++ b/src/agent/directors/identity.ts @@ -56,7 +56,9 @@ export const MODEL_ROLE_DEFAULT_EFFORT = { test: "medium", } as const satisfies Record; -export function defaultEffortForDirector(pkg: DirectorPackage): ReasoningEffort { +export function defaultEffortForDirector( + pkg: DirectorPackage, +): ReasoningEffort { if (pkg.id === "intern") return "low"; return MODEL_ROLE_DEFAULT_EFFORT[pkg.modelRole]; } diff --git a/src/agent/directors/index.ts b/src/agent/directors/index.ts index faffc7191..ca6f7c0e3 100644 --- a/src/agent/directors/index.ts +++ b/src/agent/directors/index.ts @@ -28,4 +28,7 @@ export { formatDirectorSystemPrompt, } from "./identity.js"; -export { formatBakedOptionalSkills, loadBakedSkillBody } from "./bake-skills.js"; +export { + formatBakedOptionalSkills, + loadBakedSkillBody, +} from "./bake-skills.js"; diff --git a/src/agent/directors/intern/package.test.ts b/src/agent/directors/intern/package.test.ts index f00f553ba..4f45cf82a 100644 --- a/src/agent/directors/intern/package.test.ts +++ b/src/agent/directors/intern/package.test.ts @@ -38,7 +38,13 @@ describe("internPackage", () => { expect(allow).toContain("write_file"); expect(allow).toContain("edit_file"); expect(allow).toContain("delete_file"); - for (const name of ["grep", "search_files", "spawn_agent", "wait_agents", "apply_patch"]) { + for (const name of [ + "grep", + "search_files", + "spawn_agent", + "wait_agents", + "apply_patch", + ]) { expect(allow).not.toContain(name); } }); @@ -52,7 +58,9 @@ describe("internPackage", () => { }); test("primaryIntent and description", () => { - expect(internPackage.primaryIntent).toMatch(/mechanical|exact|zero judgment/i); + expect(internPackage.primaryIntent).toMatch( + /mechanical|exact|zero judgment/i, + ); expect(internPackage.description).toBe("Mechanical intern"); }); diff --git a/src/agent/directors/intern/package.ts b/src/agent/directors/intern/package.ts index 496b9d2a2..e7fae5f49 100644 --- a/src/agent/directors/intern/package.ts +++ b/src/agent/directors/intern/package.ts @@ -7,7 +7,8 @@ import { INTERN_TOOLS } from "../tool-sets.js"; */ export const internPackage: DirectorPackage = { id: "intern", - primaryIntent: "Execute clear mechanical instructions exactly — zero judgment, zero invention", + primaryIntent: + "Execute clear mechanical instructions exactly — zero judgment, zero invention", outOfLane: [ "debugging failures or inventing fixes", "decisions not covered by the brief", diff --git a/src/agent/directors/neckbeard/package.test.ts b/src/agent/directors/neckbeard/package.test.ts index f3c564ea3..74102af42 100644 --- a/src/agent/directors/neckbeard/package.test.ts +++ b/src/agent/directors/neckbeard/package.test.ts @@ -77,11 +77,17 @@ describe("neckbeardPackage", () => { }); test("optionalSkills are style and philosophy", () => { - expect(neckbeardPackage.optionalSkills).toEqual(["style", "philosophy", "native-integration"]); + expect(neckbeardPackage.optionalSkills).toEqual([ + "style", + "philosophy", + "native-integration", + ]); }); test("primaryIntent and outOfLane match neckbeard lane", () => { - expect(neckbeardPackage.primaryIntent).toBe("Adversarial pedantic review; never fix"); + expect(neckbeardPackage.primaryIntent).toBe( + "Adversarial pedantic review; never fix", + ); expect(neckbeardPackage.outOfLane).toContain("applying fixes"); expect(neckbeardPackage.outOfLane).toContain("product implementation"); expect(neckbeardPackage.outOfLane).toContain("architecture ownership"); diff --git a/src/agent/directors/rand/package.test.ts b/src/agent/directors/rand/package.test.ts index 7f37e2d09..f05442e9e 100644 --- a/src/agent/directors/rand/package.test.ts +++ b/src/agent/directors/rand/package.test.ts @@ -86,7 +86,11 @@ describe("randPackage", () => { }); test("primaryIntent and outOfLane match rand lane", () => { - expect(randPackage.primaryIntent).toBe("Own DESIGN.md create/use + brand gate"); - expect(randPackage.outOfLane).toContain("arbitrary product code outside DESIGN.md"); + expect(randPackage.primaryIntent).toBe( + "Own DESIGN.md create/use + brand gate", + ); + expect(randPackage.outOfLane).toContain( + "arbitrary product code outside DESIGN.md", + ); }); }); diff --git a/src/agent/directors/registry.test.ts b/src/agent/directors/registry.test.ts index d2199ea02..e3e7a05b1 100644 --- a/src/agent/directors/registry.test.ts +++ b/src/agent/directors/registry.test.ts @@ -95,7 +95,9 @@ describe("director registry", () => { const explorer = packageToProfile(DIRECTOR_REGISTRY.explorer); expect(explorer.id).toBe("explorer"); expect(explorer.systemPromptRole).toContain("agent id `explorer`"); - expect(explorer.systemPromptRole).toContain(DIRECTOR_REGISTRY.explorer.systemPrompt); + expect(explorer.systemPromptRole).toContain( + DIRECTOR_REGISTRY.explorer.systemPrompt, + ); expect(explorer.description).toContain("agent id: explorer"); expect(explorer.capabilities?.mode).toBe("allow"); expect(explorer.capabilities?.tools).toContain("read_file"); @@ -123,7 +125,11 @@ describe("director registry", () => { test("greybeard spawn allowlist is intern/explorer/critic only", () => { const g = DIRECTOR_REGISTRY.greybeard; expect(g.spawn.maySpawn).toBe(true); - expect(g.spawn.allowlist?.slice().sort()).toEqual(["critic", "explorer", "intern"]); + expect(g.spawn.allowlist?.slice().sort()).toEqual([ + "critic", + "explorer", + "intern", + ]); expect(packageToProfile(g).orchestrator).toBe(true); }); @@ -155,7 +161,12 @@ describe("director registry", () => { test("builder mounts product writes + apply_patch; intern mounts writes without apply_patch; other leaves do not spawn", () => { expect(DIRECTOR_REGISTRY.builder.tools?.allow).toEqual( - expect.arrayContaining(["write_file", "edit_file", "delete_file", "apply_patch"]), + expect.arrayContaining([ + "write_file", + "edit_file", + "delete_file", + "apply_patch", + ]), ); const internAllow = DIRECTOR_REGISTRY.intern.tools?.allow ?? []; expect(internAllow).toContain("run_shell"); diff --git a/src/agent/directors/registry.ts b/src/agent/directors/registry.ts index b51258fca..6e94407be 100644 --- a/src/agent/directors/registry.ts +++ b/src/agent/directors/registry.ts @@ -27,39 +27,44 @@ import { } from "./types.js"; /** Intent -> default director when `spawn_agent(agent=...)` is omitted. No general director. */ -export const INTENT_DEFAULT_DIRECTOR: Readonly, DirectorId>> = - { - implement: "builder", - explore: "explorer", - plan: "counsel", - review: "critic", - }; +export const INTENT_DEFAULT_DIRECTOR: Readonly< + Record, DirectorId> +> = { + implement: "builder", + explore: "explorer", + plan: "counsel", + review: "critic", +}; /** * Closed v1 registry — full packages (prompts, envelopes, spawn, nudge, modelRole). * Worker modules own package bodies; this file only fans them in. */ -export const DIRECTOR_REGISTRY: Readonly> = { - skywalker: skywalkerPackage, - builder: builderPackage, - explorer: explorerPackage, - counsel: counselPackage, - intern: internPackage, - critic: criticPackage, - greybeard: greybeardPackage, - neckbeard: neckbeardPackage, - bruckheimer: bruckheimerPackage, - gaasbot: gaasbotPackage, - draper: draperPackage, - emil: emilPackage, - rand: randPackage, - shakespeare: shakespearePackage, - testsmith: testsmithPackage, - tester: testerPackage, -}; +export const DIRECTOR_REGISTRY: Readonly> = + { + skywalker: skywalkerPackage, + builder: builderPackage, + explorer: explorerPackage, + counsel: counselPackage, + intern: internPackage, + critic: criticPackage, + greybeard: greybeardPackage, + neckbeard: neckbeardPackage, + bruckheimer: bruckheimerPackage, + gaasbot: gaasbotPackage, + draper: draperPackage, + emil: emilPackage, + rand: randPackage, + shakespeare: shakespearePackage, + testsmith: testsmithPackage, + tester: testerPackage, + }; export function isDirectorId(value: unknown): value is DirectorId { - return typeof value === "string" && (DIRECTOR_IDS as readonly string[]).includes(value); + return ( + typeof value === "string" && + (DIRECTOR_IDS as readonly string[]).includes(value) + ); } /** Fleet authority tier for a closed director id, or undefined for non-director profiles. */ @@ -76,7 +81,9 @@ export function listDirectors(): readonly DirectorPackage[] { * Explicit `agentId` wins; otherwise intent maps to a default. * `general` never maps to a director — reclassify only. */ -export function resolveDirector(input: ResolveDirectorInput): ResolveDirectorResult { +export function resolveDirector( + input: ResolveDirectorInput, +): ResolveDirectorResult { if (input.agentId !== undefined && input.agentId !== "") { if (!isDirectorId(input.agentId)) { const known = DIRECTOR_IDS.join(", "); @@ -109,7 +116,9 @@ export function resolveDirector(input: ResolveDirectorInput): ResolveDirectorRes } /** Map package tool envelope → profile capability filter. Prefer allow (small mount). */ -export function packageToCapabilities(pkg: DirectorPackage): CapabilityFilter | undefined { +export function packageToCapabilities( + pkg: DirectorPackage, +): CapabilityFilter | undefined { const allow = pkg.tools?.allow; if (allow !== undefined && allow.length > 0) { return { mode: "allow", tools: [...allow] }; diff --git a/src/agent/directors/shakespeare/package.test.ts b/src/agent/directors/shakespeare/package.test.ts index 102b79f35..acfcdaa32 100644 --- a/src/agent/directors/shakespeare/package.test.ts +++ b/src/agent/directors/shakespeare/package.test.ts @@ -8,7 +8,9 @@ describe("shakespearePackage", () => { test("systemPrompt is non-empty and not a Placeholder", () => { expect(shakespearePackage.systemPrompt.length).toBeGreaterThan(0); - expect(shakespearePackage.systemPrompt.startsWith("Placeholder")).toBe(false); + expect(shakespearePackage.systemPrompt.startsWith("Placeholder")).toBe( + false, + ); }); test("systemPrompt identity is Shakespeare / ShakespeareDirector", () => { @@ -92,6 +94,8 @@ describe("shakespearePackage", () => { }); test("primaryIntent is docs maintain", () => { - expect(shakespearePackage.primaryIntent).toMatch(/docs|documentation|PRODUCT|product/i); + expect(shakespearePackage.primaryIntent).toMatch( + /docs|documentation|PRODUCT|product/i, + ); }); }); diff --git a/src/agent/directors/skywalker/package.test.ts b/src/agent/directors/skywalker/package.test.ts index 745134c0e..e2128cdfe 100644 --- a/src/agent/directors/skywalker/package.test.ts +++ b/src/agent/directors/skywalker/package.test.ts @@ -10,10 +10,16 @@ describe("skywalkerPackage", () => { expect(skywalkerPackage.systemPrompt.length).toBeGreaterThan(0); expect(skywalkerPackage.systemPrompt.startsWith("Placeholder")).toBe(false); expect(skywalkerPackage.systemPrompt).toContain("You are Skywalker"); - expect(skywalkerPackage.systemPrompt).toContain("When asked your name, answer: Skywalker"); + expect(skywalkerPackage.systemPrompt).toContain( + "When asked your name, answer: Skywalker", + ); expect(skywalkerPackage.systemPrompt).toContain("PRIMARY INTENT"); - expect(skywalkerPackage.systemPrompt).toContain("write_file/edit_file/delete_file"); - expect(skywalkerPackage.systemPrompt).toContain("DIY tiny/single-file/one-route"); + expect(skywalkerPackage.systemPrompt).toContain( + "write_file/edit_file/delete_file", + ); + expect(skywalkerPackage.systemPrompt).toContain( + "DIY tiny/single-file/one-route", + ); }); test("createSkywalkerSystemPrompt returns package systemPrompt", () => { @@ -84,7 +90,9 @@ describe("skywalkerPackage", () => { expect(skywalkerPackage.outOfLane).toContain( "searching the repo yourself after a worker stops without finishing", ); - expect(skywalkerPackage.outOfLane).toContain("diagnostic fleets for why/how/stall questions"); + expect(skywalkerPackage.outOfLane).toContain( + "diagnostic fleets for why/how/stall questions", + ); }); test("systemPrompt parent tools tell the parent not to run long-blocking jobs", () => { @@ -117,9 +125,13 @@ describe("skywalkerPackage", () => { expect(p).not.toContain("task()"); expect(p).toContain('mode="all"'); expect(p).toContain("uncollected spawns"); - expect(p).toContain("When the fleet goes dry the runtime re-enters with collected reports"); + expect(p).toContain( + "When the fleet goes dry the runtime re-enters with collected reports", + ); expect(p).toContain("do not tight-loop wait_agents"); - expect(p).not.toContain("Present the plan when the change is large or ambiguous"); + expect(p).not.toContain( + "Present the plan when the change is large or ambiguous", + ); }); test("systemPrompt requires frequent operator updates and staying free for Enter", () => { @@ -155,7 +167,9 @@ describe("skywalkerPackage", () => { expect(p).toContain("changed** brief"); expect(p).toContain("wait for the operator"); expect(p).toContain("Do not auto-retry"); - expect(p).toContain("Identical re-dispatch of the same brief stays refused"); + expect(p).toContain( + "Identical re-dispatch of the same brief stays refused", + ); expect(p).toContain("Operator-cancel is not a re-dispatch"); expect(p).not.toContain("Then start the next worker"); expect(p).not.toContain("if the job still needs doing"); @@ -199,7 +213,9 @@ describe("skywalkerPackage", () => { expect(p).toContain("required for implement/review"); expect(p).not.toContain("Brief completeness"); expect(p).not.toContain("Prefer typed spawn"); - expect(p.indexOf("Critic stays clean-room")).toBeGreaterThan(p.indexOf("# Verify after ship")); + expect(p.indexOf("Critic stays clean-room")).toBeGreaterThan( + p.indexOf("# Verify after ship"), + ); }); test("systemPrompt does not use leaf jargon", () => { @@ -215,8 +231,12 @@ describe("skywalkerPackage", () => { expect(p).toContain("idle-send"); expect(p).toContain("target = that worker's session id"); expect(p).toContain("target = worker session id"); - expect(p).not.toMatch(/wait_agents returns status running plus a question/i); - expect(p).toMatch(/Escalate with ask_operator only when you cannot resolve it/); + expect(p).not.toMatch( + /wait_agents returns status running plus a question/i, + ); + expect(p).toMatch( + /Escalate with ask_operator only when you cannot resolve it/, + ); }); test("systemPrompt puts API signatures into implement success_criteria", () => { @@ -231,9 +251,13 @@ describe("skywalkerPackage", () => { const p = skywalkerPackage.systemPrompt; expect(p).toContain("Verify after ship"); expect(p).toContain("tester"); - expect(p).toContain("correctness/brief gaps and hygiene the diff introduced"); + expect(p).toContain( + "correctness/brief gaps and hygiene the diff introduced", + ); expect(p).toContain("That hygiene lens is not over-engineering theater"); - expect(p).toMatch(/after every delegated \*\*builder\*\* implementation.*run \*\*critic\*\*/is); + expect(p).toMatch( + /after every delegated \*\*builder\*\* implementation.*run \*\*critic\*\*/is, + ); expect(p).toMatch( /substantial implementation limited to one internal file.*still requires Critic/is, ); @@ -241,7 +265,9 @@ describe("skywalkerPackage", () => { expect(p).toMatch( /After every delegated builder landing.*run a critic.*architecture.*add greybeard/is, ); - expect(p).not.toMatch(/critic \(or greybeard when architecture is in play\)/i); + expect(p).not.toMatch( + /critic \(or greybeard when architecture is in play\)/i, + ); expect(p).toMatch( /Skip a new Critic dispatch only for parent-DIY work or when existing independent review evidence already covers both the resulting diff and its success criteria/i, ); diff --git a/src/agent/directors/skywalker/package.ts b/src/agent/directors/skywalker/package.ts index 734b5ac83..09c246d8a 100644 --- a/src/agent/directors/skywalker/package.ts +++ b/src/agent/directors/skywalker/package.ts @@ -171,7 +171,8 @@ export function createSkywalkerSystemPrompt(): string { export const skywalkerPackage: DirectorPackage = { id: "skywalker", - primaryIntent: "Orchestrate; DIY tiny/bounded product edits; spawn for substantial work", + primaryIntent: + "Orchestrate; DIY tiny/bounded product edits; spawn for substantial work", outOfLane: [ "substantial multi-file product work without spawning", "docs/design authorship (PRODUCT.md, ARCHITECTURE.md, docs/design/*, brand) except one-line fixes", @@ -181,7 +182,8 @@ export const skywalkerPackage: DirectorPackage = { "diagnostic fleets for why/how/stall questions", "searching the repo yourself after a worker stops without finishing", ], - description: "Primary orchestration director — chains specialists into a workflow", + description: + "Primary orchestration director — chains specialists into a workflow", systemPrompt: SKYWALKER_SYSTEM_PROMPT, optionalSkills: ["style", "philosophy", "native-integration", "interview"], tools: { allow: SKYWALKER_TOOLS }, diff --git a/src/agent/directors/tester/package.ts b/src/agent/directors/tester/package.ts index 6ec924e42..a45cc219d 100644 --- a/src/agent/directors/tester/package.ts +++ b/src/agent/directors/tester/package.ts @@ -15,7 +15,8 @@ export const testerPackage: DirectorPackage = { "orchestration", "docs-only work", ], - description: "Runtime verify specialist — run suite/repro, report evidence, never fix", + description: + "Runtime verify specialist — run suite/repro, report evidence, never fix", systemPrompt: `You are TesterDirector (Tester), a specialist in Corbits Code. PRIMARY INTENT: run the suite / repro for the brief and report pass/fail evidence. Never fix product code. Never become the implementer. diff --git a/src/agent/directors/testsmith/package.test.ts b/src/agent/directors/testsmith/package.test.ts index 505889193..e47abccd8 100644 --- a/src/agent/directors/testsmith/package.test.ts +++ b/src/agent/directors/testsmith/package.test.ts @@ -92,7 +92,9 @@ describe("testsmithPackage", () => { test("primaryIntent is permanent-design and not primary verifier", () => { expect(testsmithPackage.primaryIntent).toMatch(/permanent test cases/i); - expect(testsmithPackage.primaryIntent).toMatch(/not.*verifier|do not run as primary verifier/i); + expect(testsmithPackage.primaryIntent).toMatch( + /not.*verifier|do not run as primary verifier/i, + ); }); test("outOfLane refuses product implement, verifier role, and landing tests", () => { diff --git a/src/agent/directors/tool-sets.test.ts b/src/agent/directors/tool-sets.test.ts index 16532fa41..622a66a90 100644 --- a/src/agent/directors/tool-sets.test.ts +++ b/src/agent/directors/tool-sets.test.ts @@ -12,7 +12,11 @@ import { describe("PRODUCT_WRITE_TOOLS", () => { test("is write_file / edit_file / delete_file", () => { - expect([...PRODUCT_WRITE_TOOLS]).toEqual(["write_file", "edit_file", "delete_file"]); + expect([...PRODUCT_WRITE_TOOLS]).toEqual([ + "write_file", + "edit_file", + "delete_file", + ]); }); }); @@ -78,12 +82,16 @@ describe("SKYWALKER_TOOLS / ORCHESTRATOR_TOOLS", () => { // CL-7051: fleet discovery is Tier-1 only. test("search_agents is on Skywalker only, not the nested orchestrator surface", () => { expect(SKYWALKER_TOOLS as readonly string[]).toContain("search_agents"); - expect(ORCHESTRATOR_TOOLS as readonly string[]).not.toContain("search_agents"); + expect(ORCHESTRATOR_TOOLS as readonly string[]).not.toContain( + "search_agents", + ); }); test("skill_search is not on Skywalker or worker orchestrator allowlists", () => { expect(SKYWALKER_TOOLS as readonly string[]).not.toContain("skill_search"); - expect(ORCHESTRATOR_TOOLS as readonly string[]).not.toContain("skill_search"); + expect(ORCHESTRATOR_TOOLS as readonly string[]).not.toContain( + "skill_search", + ); }); }); @@ -99,7 +107,12 @@ describe("REVIEW_TOOLS / INTERN_TOOLS", () => { expect(INTERN_TOOLS).toContain("run_shell"); expect(INTERN_TOOLS).toContain("read_file"); expect(INTERN_TOOLS).toContain("list_dir"); - for (const name of ["grep", "search_files", "spawn_agent", "wait_agents"] as const) { + for (const name of [ + "grep", + "search_files", + "spawn_agent", + "wait_agents", + ] as const) { expect(INTERN_TOOLS as readonly string[]).not.toContain(name); } }); diff --git a/src/agent/directors/tool-sets.ts b/src/agent/directors/tool-sets.ts index 52eb13834..39f18288c 100644 --- a/src/agent/directors/tool-sets.ts +++ b/src/agent/directors/tool-sets.ts @@ -22,7 +22,11 @@ export const READ_TOOLS = [ * build/docs only — review/explore/orchestrator/intern mount these path tools * alone (lane discipline lives in prompts, not the capability filter). */ -export const PRODUCT_WRITE_TOOLS = ["write_file", "edit_file", "delete_file"] as const; +export const PRODUCT_WRITE_TOOLS = [ + "write_file", + "edit_file", + "delete_file", +] as const; /** * Build: read + full file mutation. `shell` and `update_plan` are Codex @@ -60,7 +64,12 @@ export const DOCS_TOOLS = [ export const REVIEW_TOOLS = [...READ_TOOLS, ...PRODUCT_WRITE_TOOLS] as const; /** Mechanical intern: shell-first + path writes when the brief requires them. */ -export const INTERN_TOOLS = ["run_shell", "read_file", "list_dir", ...PRODUCT_WRITE_TOOLS] as const; +export const INTERN_TOOLS = [ + "run_shell", + "read_file", + "list_dir", + ...PRODUCT_WRITE_TOOLS, +] as const; /** Nested orchestrator surface (greybeard / package filter): dispatch + path writes. */ export const ORCHESTRATOR_TOOLS = [ @@ -77,4 +86,7 @@ export const ORCHESTRATOR_TOOLS = [ ] as const; /** Skywalker primary: orchestrator surface plus fleet discovery (Tier-1 only). */ -export const SKYWALKER_TOOLS = [...ORCHESTRATOR_TOOLS, "search_agents"] as const; +export const SKYWALKER_TOOLS = [ + ...ORCHESTRATOR_TOOLS, + "search_agents", +] as const; diff --git a/src/agent/directors/types.ts b/src/agent/directors/types.ts index 66577ed45..a77e51b33 100644 --- a/src/agent/directors/types.ts +++ b/src/agent/directors/types.ts @@ -24,7 +24,12 @@ export const DIRECTOR_IDS = [ export type DirectorId = (typeof DIRECTOR_IDS)[number]; -export type TaskIntent = "explore" | "implement" | "plan" | "review" | "general"; +export type TaskIntent = + | "explore" + | "implement" + | "plan" + | "review" + | "general"; /** * Fleet authority tier (CL-6941). Runtime-enforced at the tool-mount point in @@ -40,7 +45,13 @@ export type SubagentTier = "orchestrator" | "nested-orchestrator" | "leaf"; /** Static model-role tag used by resolveEffortForRole / defaultEffortForDirector. */ export type ModelRole = - "orchestrator" | "implement" | "explore" | "review" | "plan" | "docs" | "test"; + | "orchestrator" + | "implement" + | "explore" + | "review" + | "plan" + | "docs" + | "test"; export interface ToolEnvelope { /** Tools mounted when present — prefer small allowlists over deny-everything. */ diff --git a/src/agent/environment.test.ts b/src/agent/environment.test.ts index 9acce663d..2a1462289 100644 --- a/src/agent/environment.test.ts +++ b/src/agent/environment.test.ts @@ -16,7 +16,8 @@ function captureStderr(): { output: () => string; restore: () => void } { const original = process.stderr.write.bind(process.stderr); let wrote = ""; process.stderr.write = ((chunk: string | Uint8Array) => { - wrote += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + wrote += + typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); return true; }) as typeof process.stderr.write; return { @@ -52,7 +53,11 @@ test("gatherEnvironment detects a git work tree and lists its top level", async await mkdir(join(dir, "src")); await writeFile(join(dir, "src", "seed.ts"), "export const seed = 1;\n"); await run("git", ["add", "."], { cwd: dir }); - await run("git", ["-c", "core.hooksPath=/dev/null", "commit", "-m", "seed"], { cwd: dir }); + await run( + "git", + ["-c", "core.hooksPath=/dev/null", "commit", "-m", "seed"], + { cwd: dir }, + ); const env = await gatherEnvironment(dir); expect(env.isGitRepo).toBe(true); @@ -70,7 +75,11 @@ test("gatherEnvironment gathers branch and dirty status from the same work tree" await run("git", ["checkout", "-b", "trunk"], { cwd: dir }); await writeFile(join(dir, "seed.txt"), "seed"); await run("git", ["add", "."], { cwd: dir }); - await run("git", ["-c", "core.hooksPath=/dev/null", "commit", "-m", "seed"], { cwd: dir }); + await run( + "git", + ["-c", "core.hooksPath=/dev/null", "commit", "-m", "seed"], + { cwd: dir }, + ); await writeFile(join(dir, "a.txt"), "one"); await writeFile(join(dir, "b.txt"), "two"); diff --git a/src/agent/environment.ts b/src/agent/environment.ts index 41d50e040..1c97f7540 100644 --- a/src/agent/environment.ts +++ b/src/agent/environment.ts @@ -25,7 +25,10 @@ const GIT_TIMEOUT_MS = 3000; async function git(cwd: string, args: string[]): Promise { try { const stdout = await new Promise((resolve, reject) => { - const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] }); + const child = spawn("git", args, { + cwd, + stdio: ["ignore", "pipe", "ignore"], + }); if (child.stdout === null) { reject(new Error("git stdout is not available")); return; @@ -59,7 +62,10 @@ export type GitRunner = (cwd: string, args: string[]) => Promise; // Detached HEAD (or a repo with zero commits) makes rev-parse print "HEAD" // itself rather than a branch name; treat that as "no branch". -export async function getGitBranch(cwd: string, runGit: GitRunner = git): Promise { +export async function getGitBranch( + cwd: string, + runGit: GitRunner = git, +): Promise { const branch = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]); if (branch === null || branch.length === 0 || branch === "HEAD") return null; return branch; @@ -101,10 +107,18 @@ async function gatherTopLevel(cwd: string): Promise { } } -export async function gatherEnvironment(cwd: string, date = new Date()): Promise { - const [gitInfo, topLevel] = await Promise.all([gatherGit(cwd), gatherTopLevel(cwd)]); +export async function gatherEnvironment( + cwd: string, + date = new Date(), +): Promise { + const [gitInfo, topLevel] = await Promise.all([ + gatherGit(cwd), + gatherTopLevel(cwd), + ]); const runtime = - typeof Bun !== "undefined" ? `Bun ${Bun.version}` : `Node ${process.versions.node}`; + typeof Bun !== "undefined" + ? `Bun ${Bun.version}` + : `Node ${process.versions.node}`; return { cwd, platform: `${osType()} ${release()}`, diff --git a/src/agent/exa-web-fetch-alias.test.ts b/src/agent/exa-web-fetch-alias.test.ts index 75b710cbb..74ba77753 100644 --- a/src/agent/exa-web-fetch-alias.test.ts +++ b/src/agent/exa-web-fetch-alias.test.ts @@ -5,9 +5,15 @@ import { join } from "node:path"; import type { ToolResult } from "@intx/types/runtime"; import { stringTool, type AgentTool } from "@intx/agent"; import { withMockedModule } from "../../tests/helpers/mock-module.js"; -import { createExaMCPServerConfig, type ResolvedMCPServerConfig } from "../mcp/exa.js"; +import { + createExaMCPServerConfig, + type ResolvedMCPServerConfig, +} from "../mcp/exa.js"; import type { MCPConnectOptions } from "../mcp/client.js"; -import { createGlobalSettingsWriter, persistGlobalHTTPMCPServer } from "../mcp/add-server.js"; +import { + createGlobalSettingsWriter, + persistGlobalHTTPMCPServer, +} from "../mcp/add-server.js"; import { createPermissionGate } from "../permission/gate.js"; const dirs: string[] = []; @@ -18,7 +24,11 @@ function tempDir(prefix: string): string { return dir; } -const calls: { toolName: string; args: Record; signal: AbortSignal }[] = []; +const calls: { + toolName: string; + args: Record; + signal: AbortSignal; +}[] = []; const closedClients: string[] = []; const closedGenerations: number[] = []; let connectGeneration = 0; @@ -28,14 +38,22 @@ let releaseDeferredConnect: (() => void) | undefined; let authWaitAborts = 0; let authResourceCloses = 0; let blockInteractiveAuth = false; -let connectMode: "success" | "missing-fetch" | "failed" | "rejected" | "auth" | "deferred" = - "success"; +let connectMode: + | "success" + | "missing-fetch" + | "failed" + | "rejected" + | "auth" + | "deferred" = "success"; await withMockedModule( import.meta.resolve("../mcp/client.js"), (real: typeof import("../mcp/client.js")) => ({ ...real, - connectMCPServer: async (config: ResolvedMCPServerConfig, options: MCPConnectOptions = {}) => { + connectMCPServer: async ( + config: ResolvedMCPServerConfig, + options: MCPConnectOptions = {}, + ) => { connectConfigs.push(config); connectOptions.push(options); const generation = ++connectGeneration; @@ -55,16 +73,25 @@ await withMockedModule( options.signal?.addEventListener("abort", onAbort, { once: true }); } }); - return { ok: false, serverName: config.name, error: "authorization aborted" }; + return { + ok: false, + serverName: config.name, + error: "authorization aborted", + }; } if (connectMode === "deferred") { await new Promise((resolve) => { releaseDeferredConnect = resolve; }); } - if (connectMode === "rejected") throw new Error("transport setup exploded"); + if (connectMode === "rejected") + throw new Error("transport setup exploded"); if (connectMode === "failed") { - return { ok: false, serverName: config.name, error: "connection exploded" }; + return { + ok: false, + serverName: config.name, + error: "connection exploded", + }; } return { ok: true, @@ -72,12 +99,30 @@ await withMockedModule( serverName: config.name, tools: connectMode === "missing-fetch" - ? [{ name: "web_search_exa", description: "Search", inputSchema: {} }] + ? [ + { + name: "web_search_exa", + description: "Search", + inputSchema: {}, + }, + ] : [ - { name: "web_fetch_exa", description: "Fetch", inputSchema: {} }, - { name: "web_search_exa", description: "Search", inputSchema: {} }, + { + name: "web_fetch_exa", + description: "Fetch", + inputSchema: {}, + }, + { + name: "web_search_exa", + description: "Search", + inputSchema: {}, + }, ], - call: async (toolName: string, args: Record, signal: AbortSignal) => { + call: async ( + toolName: string, + args: Record, + signal: AbortSignal, + ) => { calls.push({ toolName, args, signal }); return "exa fetch result"; }, @@ -116,7 +161,9 @@ async function makeToolset( }); } -async function connect(toolset: Awaited>) { +async function connect( + toolset: Awaited>, +) { await toolset.connectMCP({ interactiveAuth: false, onStatus: () => undefined, @@ -130,7 +177,10 @@ async function runTool( args: Record, signal = new AbortController().signal, ): Promise { - return toolset.dynamicRunner.run({ id: `call-${name}`, name, arguments: args }, signal); + return toolset.dynamicRunner.run( + { id: `call-${name}`, name, arguments: args }, + signal, + ); } beforeEach(() => { @@ -157,12 +207,16 @@ describe("built-in Exa web_fetch alias", () => { test("advertises canonical web_fetch from turn 1 and hides the built-in raw fetch", async () => { const toolset = await makeToolset(); try { - const initialNames = toolset.dynamicRunner.currentDefinitions().map((d) => d.name); + const initialNames = toolset.dynamicRunner + .currentDefinitions() + .map((d) => d.name); expect(initialNames).toContain("web_fetch"); expect(initialNames).not.toContain("mcp__exa__web_fetch_exa"); await connect(toolset); - const connectedNames = toolset.dynamicRunner.currentDefinitions().map((d) => d.name); + const connectedNames = toolset.dynamicRunner + .currentDefinitions() + .map((d) => d.name); expect(connectedNames).toContain("web_fetch"); expect(connectedNames).toContain("mcp__exa__web_search_exa"); expect(connectedNames).not.toContain("mcp__exa__web_fetch_exa"); @@ -177,7 +231,9 @@ describe("built-in Exa web_fetch alias", () => { resolveMcpServers([{ name: "exa", enabled: false }], undefined), ); try { - expect(disabled.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain("web_fetch"); + expect( + disabled.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("web_fetch"); await connect(disabled); expect(connectConfigs).toHaveLength(0); } finally { @@ -193,7 +249,9 @@ describe("built-in Exa web_fetch alias", () => { ); try { await connect(custom); - const names = custom.dynamicRunner.currentDefinitions().map((d) => d.name); + const names = custom.dynamicRunner + .currentDefinitions() + .map((d) => d.name); expect(names).toContain("web_fetch"); expect(names).toContain("mcp__exa__web_fetch_exa"); expect(connectConfigs).toEqual([ @@ -217,7 +275,10 @@ describe("built-in Exa web_fetch alias", () => { ); await connecting; - expect(result).toEqual({ callId: "call-web_fetch", content: "exa fetch result" }); + expect(result).toEqual({ + callId: "call-web_fetch", + content: "exa fetch result", + }); expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ toolName: "web_fetch_exa", @@ -236,7 +297,9 @@ describe("built-in Exa web_fetch alias", () => { const toolset = await makeToolset(); try { await connect(toolset); - const result = await runTool(toolset, "web_fetch", { url: "ftp://example.com/file" }); + const result = await runTool(toolset, "web_fetch", { + url: "ftp://example.com/file", + }); expect(result).not.toHaveProperty("isError"); expect(result.content).toBe( @@ -253,7 +316,9 @@ describe("built-in Exa web_fetch alias", () => { const toolset = await makeToolset(); try { await connect(toolset); - const result = await runTool(toolset, "web_fetch", { url: "https://example.com" }); + const result = await runTool(toolset, "web_fetch", { + url: "https://example.com", + }); expect(result).not.toHaveProperty("isError"); expect(result.content).toContain("Exa MCP"); expect(result.content).toContain("web_fetch_exa"); @@ -266,7 +331,9 @@ describe("built-in Exa web_fetch alias", () => { const failed = await makeToolset(); try { await connect(failed); - const result = await runTool(failed, "web_fetch", { url: "https://example.com" }); + const result = await runTool(failed, "web_fetch", { + url: "https://example.com", + }); expect(result).not.toHaveProperty("isError"); expect(result.content).toContain("Exa MCP"); expect(result.content).toContain("connection exploded"); @@ -284,10 +351,15 @@ describe("built-in Exa web_fetch alias", () => { const states: { state: string; url?: string }[] = []; const callbacks = { interactiveAuth: true, - onStatus: (status: { state: string; url?: string }) => states.push(status), + onStatus: (status: { state: string; url?: string }) => + states.push(status), onToolsChanged: () => undefined, }; - const server = { name: "linear", type: "http" as const, url: "https://mcp.linear.app/mcp" }; + const server = { + name: "linear", + type: "http" as const, + url: "https://mcp.linear.app/mcp", + }; try { await Promise.all([ toolset.connectMCPServer(server, callbacks), @@ -336,7 +408,9 @@ describe("built-in Exa web_fetch alias", () => { expect(states).toEqual(["connecting"]); expect(closedClients).toEqual(["linear"]); expect( - toolset.dynamicRunner.currentDefinitions().some((tool) => tool.name.includes("linear")), + toolset.dynamicRunner + .currentDefinitions() + .some((tool) => tool.name.includes("linear")), ).toBe(false); }); @@ -372,7 +446,9 @@ describe("built-in Exa web_fetch alias", () => { expect(authResourceCloses).toBe(1); expect(states).toEqual(["connecting", "needs-auth"]); expect( - toolset.dynamicRunner.currentDefinitions().some((tool) => tool.name.includes("linear")), + toolset.dynamicRunner + .currentDefinitions() + .some((tool) => tool.name.includes("linear")), ).toBe(false); }); @@ -469,7 +545,10 @@ describe("built-in Exa web_fetch alias", () => { }, ); - expect(states.map((status) => status.state)).toEqual(["connecting", "failed"]); + expect(states.map((status) => status.state)).toEqual([ + "connecting", + "failed", + ]); expect(states[1]?.error).toContain("registration exploded"); expect(closedClients).toEqual(["linear"]); } finally { @@ -492,7 +571,11 @@ describe("built-in Exa web_fetch alias", () => { resolveMcpServers([{ name: "exa", enabled: false }], undefined), gate, ); - const server = { name: "linear", type: "http" as const, url: "https://mcp.linear.app/mcp" }; + const server = { + name: "linear", + type: "http" as const, + url: "https://mcp.linear.app/mcp", + }; const states: { state: string; error?: string }[] = []; try { await toolset.connectMCPServer(server, { @@ -501,13 +584,18 @@ describe("built-in Exa web_fetch alias", () => { onToolsChanged: () => undefined, }); - expect(states.map((status) => status.state)).toEqual(["connecting", "failed"]); + expect(states.map((status) => status.state)).toEqual([ + "connecting", + "failed", + ]); expect(states[1]?.error).toContain("transport setup exploded"); expect(registrations).toBe(0); expect(unregistrations).toBe(0); expect(closedClients).toEqual([]); expect( - toolset.dynamicRunner.currentDefinitions().some((tool) => tool.name.includes("linear")), + toolset.dynamicRunner + .currentDefinitions() + .some((tool) => tool.name.includes("linear")), ).toBe(false); expect(toolset.hasMCPServer("linear")).toBe(false); @@ -548,11 +636,16 @@ describe("built-in Exa web_fetch alias", () => { onToolsChanged: () => undefined, }); - expect(states.map((status) => status.state)).toEqual(["connecting", "failed"]); + expect(states.map((status) => status.state)).toEqual([ + "connecting", + "failed", + ]); expect(states[1]?.error).toContain("connection exploded"); expect(toolset.hasMCPServer("linear")).toBe(false); expect(await Bun.file(path).json()).toMatchObject({ - mcpServers: [{ name: "linear", type: "http", url: "https://mcp.linear.app/mcp" }], + mcpServers: [ + { name: "linear", type: "http", url: "https://mcp.linear.app/mcp" }, + ], }); expect( await persistGlobalHTTPMCPServer( @@ -574,7 +667,9 @@ describe("built-in Exa web_fetch alias", () => { expect(retryStates).toEqual(["connecting", "connected"]); expect(toolset.hasMCPServer("linear")).toBe(true); expect(await Bun.file(path).json()).toMatchObject({ - mcpServers: [{ name: "linear", type: "http", url: "https://mcp.linear.app/mcp" }], + mcpServers: [ + { name: "linear", type: "http", url: "https://mcp.linear.app/mcp" }, + ], }); } finally { await toolset.dispose(); @@ -584,7 +679,11 @@ describe("built-in Exa web_fetch alias", () => { test("child assembly keeps inherited canonical web_fetch and avoids duplicate native fetch", () => { const inherited: AgentTool[] = [ stringTool({ - definition: { name: "web_fetch", description: "Inherited Exa fetch", inputSchema: {} }, + definition: { + name: "web_fetch", + description: "Inherited Exa fetch", + inputSchema: {}, + }, handler: async () => "inherited", }), ]; @@ -600,9 +699,9 @@ describe("built-in Exa web_fetch alias", () => { const toolset = await makeToolset(); try { await connect(toolset); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__exa__web_search_exa", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__exa__web_search_exa"); await toolset.disconnectMCPServer("exa", { interactiveAuth: false, @@ -610,12 +709,17 @@ describe("built-in Exa web_fetch alias", () => { onToolsChanged: () => undefined, }); - const names = toolset.dynamicRunner.currentDefinitions().map((d) => d.name); + const names = toolset.dynamicRunner + .currentDefinitions() + .map((d) => d.name); expect(names).toContain("web_fetch"); expect(names.some((name) => name.startsWith("mcp__exa__"))).toBe(false); calls.length = 0; - const result = await runTool(toolset, "web_fetch", { url: "http://127.0.0.1:1", timeout: 1 }); + const result = await runTool(toolset, "web_fetch", { + url: "http://127.0.0.1:1", + timeout: 1, + }); expect(calls).toHaveLength(0); expect(String(result.content)).not.toBe("exa fetch result"); expect(String(result.content)).not.toContain("Exa MCP"); @@ -634,7 +738,10 @@ describe("built-in Exa web_fetch alias", () => { }); calls.length = 0; - const native = await runTool(toolset, "web_fetch", { url: "http://127.0.0.1:1", timeout: 1 }); + const native = await runTool(toolset, "web_fetch", { + url: "http://127.0.0.1:1", + timeout: 1, + }); expect(calls).toHaveLength(0); expect(String(native.content)).not.toContain("Exa MCP"); expect(toolset.hasMCPServer("exa")).toBe(false); @@ -647,13 +754,18 @@ describe("built-in Exa web_fetch alias", () => { }); expect(connectConfigs.length).toBe(connectsBefore + 1); expect(toolset.hasMCPServer("exa")).toBe(true); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__exa__web_search_exa", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__exa__web_search_exa"); calls.length = 0; - const aliased = await runTool(toolset, "web_fetch", { url: "https://example.com" }); - expect(aliased).toEqual({ callId: "call-web_fetch", content: "exa fetch result" }); + const aliased = await runTool(toolset, "web_fetch", { + url: "https://example.com", + }); + expect(aliased).toEqual({ + callId: "call-web_fetch", + content: "exa fetch result", + }); expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ toolName: "web_fetch_exa", @@ -670,7 +782,10 @@ describe("built-in Exa web_fetch alias", () => { ); try { calls.length = 0; - const native = await runTool(toolset, "web_fetch", { url: "http://127.0.0.1:1", timeout: 1 }); + const native = await runTool(toolset, "web_fetch", { + url: "http://127.0.0.1:1", + timeout: 1, + }); expect(calls).toHaveLength(0); expect(String(native.content)).not.toContain("Exa MCP"); @@ -680,13 +795,18 @@ describe("built-in Exa web_fetch alias", () => { onToolsChanged: () => undefined, }); expect(toolset.hasMCPServer("exa")).toBe(true); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__exa__web_search_exa", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__exa__web_search_exa"); calls.length = 0; - const aliased = await runTool(toolset, "web_fetch", { url: "https://example.com" }); - expect(aliased).toEqual({ callId: "call-web_fetch", content: "exa fetch result" }); + const aliased = await runTool(toolset, "web_fetch", { + url: "https://example.com", + }); + expect(aliased).toEqual({ + callId: "call-web_fetch", + content: "exa fetch result", + }); expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ toolName: "web_fetch_exa", @@ -713,13 +833,18 @@ describe("built-in Exa web_fetch alias", () => { onToolsChanged: () => undefined, }); expect(connectConfigs.length).toBe(firstConnects + 1); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__exa__web_search_exa", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__exa__web_search_exa"); calls.length = 0; - const result = await runTool(toolset, "web_fetch", { url: "https://example.com" }); - expect(result).toEqual({ callId: "call-web_fetch", content: "exa fetch result" }); + const result = await runTool(toolset, "web_fetch", { + url: "https://example.com", + }); + expect(result).toEqual({ + callId: "call-web_fetch", + content: "exa fetch result", + }); expect(calls[0]?.toolName).toBe("web_fetch_exa"); } finally { await toolset.dispose(); @@ -745,15 +870,20 @@ describe("built-in Exa web_fetch alias", () => { await Promise.all([disconnecting, connecting]); expect(toolset.hasMCPServer("exa")).toBe(true); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__exa__web_search_exa", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__exa__web_search_exa"); expect(closedGenerations).toContain(1); expect(connectConfigs.length).toBe(firstConnects + 1); calls.length = 0; - const result = await runTool(toolset, "web_fetch", { url: "https://example.com" }); - expect(result).toEqual({ callId: "call-web_fetch", content: "exa fetch result" }); + const result = await runTool(toolset, "web_fetch", { + url: "https://example.com", + }); + expect(result).toEqual({ + callId: "call-web_fetch", + content: "exa fetch result", + }); expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ toolName: "web_fetch_exa", @@ -770,7 +900,9 @@ describe("built-in Exa web_fetch alias", () => { const startup = connect(toolset); while (releaseDeferredConnect === undefined) await Promise.resolve(); try { - const fetchPromise = runTool(toolset, "web_fetch", { url: "https://example.com" }); + const fetchPromise = runTool(toolset, "web_fetch", { + url: "https://example.com", + }); await Promise.resolve(); await Promise.resolve(); @@ -787,7 +919,10 @@ describe("built-in Exa web_fetch alias", () => { const result = await fetchPromise; expect(String(result.content)).not.toContain("disconnected"); - expect(result).toEqual({ callId: "call-web_fetch", content: "exa fetch result" }); + expect(result).toEqual({ + callId: "call-web_fetch", + content: "exa fetch result", + }); expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ toolName: "web_fetch_exa", diff --git a/src/agent/fleet-verbs-mount.test.ts b/src/agent/fleet-verbs-mount.test.ts index 11ce7112c..4c8762e11 100644 --- a/src/agent/fleet-verbs-mount.test.ts +++ b/src/agent/fleet-verbs-mount.test.ts @@ -61,7 +61,11 @@ describe("primary fleet verb mount", () => { getSkipPermissions: () => false, } as never; const sessions = createSubAgentSessionStore(); - const worker = sessions.start({ description: "d", agentId: "a", brief: "b" }); + const worker = sessions.start({ + description: "d", + agentId: "a", + brief: "b", + }); sessions.markRunning(worker.id); sessions.registerClose(worker.id, async () => { throw new Error("1 shell child process still live after 2000ms reap"); @@ -82,7 +86,9 @@ describe("primary fleet verb mount", () => { }, }); - await expect(toolset.dispose()).rejects.toThrow(/still live after 2000ms reap/); + await expect(toolset.dispose()).rejects.toThrow( + /still live after 2000ms reap/, + ); }); test("createAgentToolset dispose rejects when a retained completed persist worker leaves children", async () => { @@ -119,7 +125,9 @@ describe("primary fleet verb mount", () => { }, }); - await expect(toolset.dispose()).rejects.toThrow(/still live after 2000ms reap/); + await expect(toolset.dispose()).rejects.toThrow( + /still live after 2000ms reap/, + ); }); test("createAgentToolset dispose closes remaining retained workers after the first leftover", async () => { @@ -169,7 +177,9 @@ describe("primary fleet verb mount", () => { }, }); - await expect(toolset.dispose()).rejects.toThrow(/still live after 2000ms reap/); + await expect(toolset.dispose()).rejects.toThrow( + /still live after 2000ms reap/, + ); expect(firstCloseCalls).toBe(1); expect(secondCloseCalls).toBe(1); }); @@ -208,7 +218,9 @@ describe("primary fleet verb mount", () => { }, }); - await expect(toolset.dispose()).rejects.toThrow(/still live after 2000ms reap/); + await expect(toolset.dispose()).rejects.toThrow( + /still live after 2000ms reap/, + ); }); test("createAgentToolset omits fleet verbs when subAgent is not set", async () => { diff --git a/src/agent/lazy-blob-reader.test.ts b/src/agent/lazy-blob-reader.test.ts index 4515251c6..8f8d55131 100644 --- a/src/agent/lazy-blob-reader.test.ts +++ b/src/agent/lazy-blob-reader.test.ts @@ -32,16 +32,24 @@ describe("createLazyBlobReader", () => { test("throws when no backing reader is configured", async () => { const lazy = createLazyBlobReader(() => undefined); - await expect(lazy.read("tool-output:///x")).rejects.toThrow("blob reader is not configured"); + await expect(lazy.read("tool-output:///x")).rejects.toThrow( + "blob reader is not configured", + ); }); }); describe("isBlobNotFoundError", () => { test("matches store miss messages", () => { - expect(isBlobNotFoundError(new Error('Blob not found for key: "x"'))).toBe(true); - expect(isBlobNotFoundError(new Error("blob reader is not configured"))).toBe(true); + expect(isBlobNotFoundError(new Error('Blob not found for key: "x"'))).toBe( + true, + ); + expect( + isBlobNotFoundError(new Error("blob reader is not configured")), + ).toBe(true); expect( - isBlobNotFoundError(new Error('invalid tool-output URI scheme: expected "tool-output:"')), + isBlobNotFoundError( + new Error('invalid tool-output URI scheme: expected "tool-output:"'), + ), ).toBe(false); expect(isBlobNotFoundError("Blob not found")).toBe(false); }); @@ -50,14 +58,21 @@ describe("isBlobNotFoundError", () => { describe("createCompositeBlobReader", () => { test("prefers the primary store when the key is present", async () => { const child = readerWith({ shared: "from-child", childOnly: "child-only" }); - const parent = readerWith({ shared: "from-parent", parentOnly: "parent-only" }); + const parent = readerWith({ + shared: "from-parent", + parentOnly: "parent-only", + }); const composite = createCompositeBlobReader( () => child, () => parent, ); - expect(dec.decode(await composite.read("tool-output:///shared"))).toBe("from-child"); - expect(dec.decode(await composite.read("tool-output:///childOnly"))).toBe("child-only"); + expect(dec.decode(await composite.read("tool-output:///shared"))).toBe( + "from-child", + ); + expect(dec.decode(await composite.read("tool-output:///childOnly"))).toBe( + "child-only", + ); }); test("falls back to the parent store for missing child keys (sub-agent re-read)", async () => { @@ -77,7 +92,9 @@ describe("createCompositeBlobReader", () => { expect(dec.decode(await composite.read("tool-output:///parentSpill"))).toBe( "mcp-skill-body-tail", ); - expect(dec.decode(await composite.read("tool-output:///ownSpill"))).toBe("child-local"); + expect(dec.decode(await composite.read("tool-output:///ownSpill"))).toBe( + "child-local", + ); }); test("surfaces a miss when neither store has the key", async () => { @@ -85,7 +102,9 @@ describe("createCompositeBlobReader", () => { () => readerWith({}), () => readerWith({}), ); - await expect(composite.read("tool-output:///gone")).rejects.toThrow("Blob not found"); + await expect(composite.read("tool-output:///gone")).rejects.toThrow( + "Blob not found", + ); }); test("does not fall through on malformed URIs", async () => { @@ -100,7 +119,9 @@ describe("createCompositeBlobReader", () => { }, }), ); - await expect(composite.read("file:///not-a-blob")).rejects.toThrow("invalid tool-output URI"); + await expect(composite.read("file:///not-a-blob")).rejects.toThrow( + "invalid tool-output URI", + ); expect(parentTouched).toBe(false); }); @@ -117,6 +138,8 @@ describe("createCompositeBlobReader", () => { const child = readerWith({ c: "child" }); const composite = createCompositeBlobReader(() => child); expect(dec.decode(await composite.read("tool-output:///c"))).toBe("child"); - await expect(composite.read("tool-output:///missing")).rejects.toThrow("Blob not found"); + await expect(composite.read("tool-output:///missing")).rejects.toThrow( + "Blob not found", + ); }); }); diff --git a/src/agent/lazy-blob-reader.ts b/src/agent/lazy-blob-reader.ts index d5d4571ed..99420b46b 100644 --- a/src/agent/lazy-blob-reader.ts +++ b/src/agent/lazy-blob-reader.ts @@ -1,7 +1,9 @@ import type { BlobReader } from "@intx/types/runtime"; /** Blob reader that resolves the backing store on each read (e.g. after agent rebuild). */ -export function createLazyBlobReader(get: () => BlobReader | undefined): BlobReader { +export function createLazyBlobReader( + get: () => BlobReader | undefined, +): BlobReader { return { read: async (uri: string) => { const reader = get(); @@ -20,7 +22,9 @@ export function createLazyBlobReader(get: () => BlobReader | undefined): BlobRea export function isBlobNotFoundError(err: unknown): boolean { if (!(err instanceof Error)) return false; const msg = err.message; - return msg.includes("Blob not found") || msg === "blob reader is not configured"; + return ( + msg.includes("Blob not found") || msg === "blob reader is not configured" + ); } /** diff --git a/src/agent/live-tool-dispatch.test.ts b/src/agent/live-tool-dispatch.test.ts index b5d773f7a..de6b77eaf 100644 --- a/src/agent/live-tool-dispatch.test.ts +++ b/src/agent/live-tool-dispatch.test.ts @@ -12,7 +12,11 @@ const stringTool = (name: string, reply: string) => ({ definition: { name, description: name, - inputSchema: { type: "object" as const, properties: {}, required: [] as string[] }, + inputSchema: { + type: "object" as const, + properties: {}, + required: [] as string[], + }, }, handler: async () => reply, }); @@ -38,7 +42,9 @@ describe("live tool dispatch fallback", () => { ]); expect(fallbackLiveToolBundle(many)).toBeUndefined(); - const none = new Map([["read_file", { run: () => undefined }]]); + const none = new Map([ + ["read_file", { run: () => undefined }], + ]); expect(fallbackLiveToolBundle(none)).toBeUndefined(); }); diff --git a/src/agent/live-tool-dispatch.ts b/src/agent/live-tool-dispatch.ts index 24716d68b..744c76303 100644 --- a/src/agent/live-tool-dispatch.ts +++ b/src/agent/live-tool-dispatch.ts @@ -1,4 +1,9 @@ -import { createAgent, type Agent, type AgentDefinition, type BaseEnv } from "@intx/agent"; +import { + createAgent, + type Agent, + type AgentDefinition, + type BaseEnv, +} from "@intx/agent"; // XXX — @intx/agent resolveTools snapshots `byName` from each bundle's // definitions at createAgent and never consults a live getter. MCP tools @@ -37,7 +42,9 @@ export function fallbackLiveToolBundle(map: Map): V | undefined { return found; } -function createLiveDispatchMap(iterable?: Iterable | null): Map { +function createLiveDispatchMap( + iterable?: Iterable | null, +): Map { const map = new OriginalMap(iterable ?? undefined); const protoGet = OriginalMap.prototype.get.bind(map); map.get = (key: K) => { @@ -51,7 +58,9 @@ function createLiveDispatchMap(iterable?: Iterable | null // Compatible with `new Map()` inside published @intx/agent. Not a class — // we only need a constructable that returns a Map with a live get(). const LiveDispatchMap = Object.assign( - function LiveDispatchMap(iterable?: Iterable | null): Map { + function LiveDispatchMap( + iterable?: Iterable | null, + ): Map { return createLiveDispatchMap(iterable); }, { prototype: OriginalMap.prototype }, diff --git a/src/agent/lsp-availability.test.ts b/src/agent/lsp-availability.test.ts index 015930398..62456b5fa 100644 --- a/src/agent/lsp-availability.test.ts +++ b/src/agent/lsp-availability.test.ts @@ -13,7 +13,11 @@ async function tempProject(): Promise { } afterEach(async () => { - await Promise.all(dirsToClean.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); + await Promise.all( + dirsToClean + .splice(0) + .map((dir) => rm(dir, { recursive: true, force: true })), + ); }); async function seedTsserver(dir: string): Promise { @@ -31,7 +35,12 @@ describe("detectLanguageServerAvailable", () => { test("true when tsserver is resolvable and a local .bin binary exists", async () => { const dir = await tempProject(); await seedTsserver(dir); - const bin = path.join(dir, "node_modules", ".bin", "typescript-language-server"); + const bin = path.join( + dir, + "node_modules", + ".bin", + "typescript-language-server", + ); await mkdir(path.dirname(bin), { recursive: true }); await writeFile(bin, "#!/usr/bin/env node\n"); expect(detectLanguageServerAvailable(dir)).toBe(true); diff --git a/src/agent/lsp-availability.ts b/src/agent/lsp-availability.ts index 108e29058..6a6d97e92 100644 --- a/src/agent/lsp-availability.ts +++ b/src/agent/lsp-availability.ts @@ -7,9 +7,20 @@ import path from "node:path"; // by spawning a server — because the `lsp` tool's advertisement is baked into // the wire tools array for the life of the session (see tool-search.ts). export function detectLanguageServerAvailable(cwd: string): boolean { - const tsserverPath = path.join(cwd, "node_modules", "typescript", "lib", "tsserver.js"); + const tsserverPath = path.join( + cwd, + "node_modules", + "typescript", + "lib", + "tsserver.js", + ); if (!existsSync(tsserverPath)) return false; - const localBin = path.join(cwd, "node_modules", ".bin", "typescript-language-server"); + const localBin = path.join( + cwd, + "node_modules", + ".bin", + "typescript-language-server", + ); if (existsSync(localBin)) return true; return Bun.which("typescript-language-server") !== null; } diff --git a/src/agent/message-provenance.ts b/src/agent/message-provenance.ts index ef5e084fc..8c3e7d6df 100644 --- a/src/agent/message-provenance.ts +++ b/src/agent/message-provenance.ts @@ -17,6 +17,8 @@ */ export const OPERATOR_ORIGINATED_FLAG = "operator-originated"; -export function isOperatorOriginated(flags: readonly string[] | undefined): boolean { +export function isOperatorOriginated( + flags: readonly string[] | undefined, +): boolean { return flags !== undefined && flags.includes(OPERATOR_ORIGINATED_FLAG); } diff --git a/src/agent/model-family-policy.test.ts b/src/agent/model-family-policy.test.ts index 94ff8d3ca..f25c0ec16 100644 --- a/src/agent/model-family-policy.test.ts +++ b/src/agent/model-family-policy.test.ts @@ -13,15 +13,24 @@ describe("resolveModelFamilyPolicy", () => { }); test("grok shares the default sub-agent stall timeout (thinking gaps are long)", () => { - const grok = resolveModelFamilyPolicy({ providerName: "xai/default", model: "grok-4.5" }); - const base = resolveModelFamilyPolicy({ providerName: "anthropic", model: "claude-sonnet-4" }); + const grok = resolveModelFamilyPolicy({ + providerName: "xai/default", + model: "grok-4.5", + }); + const base = resolveModelFamilyPolicy({ + providerName: "anthropic", + model: "claude-sonnet-4", + }); expect(grok.family).toBe("grok"); expect(grok.toolOnlyTurnNudgeAt).toBe(base.toolOnlyTurnNudgeAt); expect(grok.subAgentStallTimeoutMs).toBe(base.subAgentStallTimeoutMs); }); test("grok finish-bias applies to leaves but not orchestrators", () => { - const leaf = resolveModelFamilyPolicy({ providerName: "xai/default", orchestrator: false }); + const leaf = resolveModelFamilyPolicy({ + providerName: "xai/default", + orchestrator: false, + }); const orchestrator = resolveModelFamilyPolicy({ providerName: "xai/default", orchestrator: true, @@ -31,8 +40,14 @@ describe("resolveModelFamilyPolicy", () => { }); test("kimi is detected but ships the permissive default thresholds", () => { - const kimi = resolveModelFamilyPolicy({ providerName: "moonshot", model: "kimi-k2" }); - const base = resolveModelFamilyPolicy({ providerName: "anthropic", model: "claude-sonnet-4" }); + const kimi = resolveModelFamilyPolicy({ + providerName: "moonshot", + model: "kimi-k2", + }); + const base = resolveModelFamilyPolicy({ + providerName: "anthropic", + model: "claude-sonnet-4", + }); expect(kimi.family).toBe("kimi"); expect(kimi.toolOnlyTurnNudgeAt).toBe(base.toolOnlyTurnNudgeAt); expect(kimi.subAgentStallTimeoutMs).toBe(base.subAgentStallTimeoutMs); diff --git a/src/agent/model-family-policy.ts b/src/agent/model-family-policy.ts index 08d135bde..30aa90824 100644 --- a/src/agent/model-family-policy.ts +++ b/src/agent/model-family-policy.ts @@ -1,4 +1,7 @@ -import { detectModelFamily, type ModelFamily } from "../subagent/provider-family.js"; +import { + detectModelFamily, + type ModelFamily, +} from "../subagent/provider-family.js"; /** * Per-model-family tuning for the shared directors (main chat director and @@ -86,7 +89,10 @@ export function resolveModelFamilyPolicy(input: { // The finish-bias residual only makes sense on leaf workers, mirroring // shouldApplyGrokAntiThrash: orchestrators dispatch other agents rather // than doing the work directly. - return { ...policy, applyGrokFinishBias: policy.applyGrokFinishBias && !orchestrator }; + return { + ...policy, + applyGrokFinishBias: policy.applyGrokFinishBias && !orchestrator, + }; } case "kimi": return { family, ...KIMI_POLICY }; diff --git a/src/agent/posix-tool-plugins.test.ts b/src/agent/posix-tool-plugins.test.ts index 6ae08a40f..950ba0e9c 100644 --- a/src/agent/posix-tool-plugins.test.ts +++ b/src/agent/posix-tool-plugins.test.ts @@ -8,11 +8,17 @@ import type { ToolPlugin } from "@intx/tools-posix"; import type { ToolCall, ToolResult } from "@intx/types/runtime"; import { createPermissionGate } from "../permission/gate.js"; import { buildCorePosixToolPlugins } from "./posix-tool-plugins.js"; -import { createCompositeBlobReader, createLazyBlobReader } from "./lazy-blob-reader.js"; +import { + createCompositeBlobReader, + createLazyBlobReader, +} from "./lazy-blob-reader.js"; import { verifyPlugin } from "../plugins/verify-plugin.js"; import { editFileLineRangePlugin } from "../plugins/edit-file-line-range-plugin.js"; -type ToolHandlerLike = (call: ToolCall, signal: AbortSignal) => Promise; +type ToolHandlerLike = ( + call: ToolCall, + signal: AbortSignal, +) => Promise; /** * editFileLineRangePlugin never calls `next` for start_line/end_line edits (it @@ -25,7 +31,9 @@ function findMiddlewareIndex( plugins: ReturnType, marker: string, ): number { - return plugins.findIndex((plugin) => plugin.middleware?.toString().includes(marker) === true); + return plugins.findIndex( + (plugin) => plugin.middleware?.toString().includes(marker) === true, + ); } describe("buildCorePosixToolPlugins", () => { @@ -84,7 +92,11 @@ describe("buildCorePosixToolPlugins", () => { }); const outPath = join(cwd, "out.txt"); const denied = await runner.run( - { id: "1", name: "write_file", arguments: { path: outPath, content: "nope" } }, + { + id: "1", + name: "write_file", + arguments: { path: outPath, content: "nope" }, + }, new AbortController().signal, ); expect(denied.isError).toBe(true); @@ -109,7 +121,9 @@ describe("buildCorePosixToolPlugins", () => { // The read-file guard caps the read before result-truncation would run, // so a 90KB single line comes back line-truncated and bounded. expect(String(allowed.content)).toContain("line truncated at 2000 chars"); - expect(Buffer.byteLength(String(allowed.content), "utf8")).toBeLessThan(4096); + expect(Buffer.byteLength(String(allowed.content), "utf8")).toBeLessThan( + 4096, + ); } finally { await rm(cwd, { recursive: true, force: true }); } @@ -263,7 +277,8 @@ describe("buildCorePosixToolPlugins", () => { const childHolder: { current?: ReturnType } = {}; const parentReader = createBlobReader({ async readBlob(key: string) { - if (key === "parent-mcp-skill") return encoder.encode("parent-skill-body-tail"); + if (key === "parent-mcp-skill") + return encoder.encode("parent-skill-body-tail"); throw new Error(`Blob not found for key: ${JSON.stringify(key)}`); }, }); @@ -346,8 +361,14 @@ describe("buildCorePosixToolPlugins", () => { }); const plugins = buildCorePosixToolPlugins({ cwd, permissionGate: gate }); - const verifyIndex = findMiddlewareIndex(plugins, "Edit verification failed"); - const editRangeIndex = findMiddlewareIndex(plugins, "runEditFileLineRange"); + const verifyIndex = findMiddlewareIndex( + plugins, + "Edit verification failed", + ); + const editRangeIndex = findMiddlewareIndex( + plugins, + "runEditFileLineRange", + ); expect(verifyIndex).toBeGreaterThanOrEqual(0); expect(editRangeIndex).toBeGreaterThanOrEqual(0); @@ -408,11 +429,16 @@ describe("buildCorePosixToolPlugins", () => { return next(call, signal); }; - const base: ToolHandlerLike = async (call) => ({ callId: call.id, content: "unreachable" }); + const base: ToolHandlerLike = async (call) => ({ + callId: call.id, + content: "unreachable", + }); const verifyMiddleware = verifyPlugin().middleware; const editRangeMiddleware = editFileLineRangePlugin().middleware; if (verifyMiddleware === undefined || editRangeMiddleware === undefined) { - throw new Error("expected verifyPlugin and editFileLineRangePlugin to expose middleware"); + throw new Error( + "expected verifyPlugin and editFileLineRangePlugin to expose middleware", + ); } const composed = composeMiddleware( [verifyMiddleware, concurrentWriterMiddleware, editRangeMiddleware], @@ -456,7 +482,11 @@ describe("buildCorePosixToolPlugins", () => { }); const result = await runner.run( - { id: "grep-1", name: "grep", arguments: { pattern: "AKIA|sk-", path: cwd } }, + { + id: "grep-1", + name: "grep", + arguments: { pattern: "AKIA|sk-", path: cwd }, + }, new AbortController().signal, ); @@ -503,7 +533,10 @@ describe("buildCorePosixToolPlugins", () => { reactorGated: false, cwd: "/tmp", }); - const plugins = buildCorePosixToolPlugins({ cwd: "/tmp", permissionGate: gate }); + const plugins = buildCorePosixToolPlugins({ + cwd: "/tmp", + permissionGate: gate, + }); const ripgrepIndex = findMiddlewareIndex(plugins, "no matches for /"); expect(ripgrepIndex).toBeGreaterThanOrEqual(0); plugins[ripgrepIndex] = shortCircuitingPlugin; @@ -536,7 +569,8 @@ describe("buildCorePosixToolPlugins", () => { // the scrub, a secret split mid-pattern at the cap boundary would no // longer match the scrub's regex, and a bare, unredacted fragment of the // credential would reach the model with no redaction marker at all. - const { MAX_RESULT_CHARS } = await import("../plugins/result-truncation-plugin.js"); + const { MAX_RESULT_CHARS } = + await import("../plugins/result-truncation-plugin.js"); // A newline immediately ahead of the key gives the scrub regex's `\b` a // real word boundary; the padding length puts the cap boundary partway // through the 20-char key that follows, so a truncate-then-scrub bug @@ -560,7 +594,10 @@ describe("buildCorePosixToolPlugins", () => { reactorGated: false, cwd: "/tmp", }); - const plugins = buildCorePosixToolPlugins({ cwd: "/tmp", permissionGate: gate }); + const plugins = buildCorePosixToolPlugins({ + cwd: "/tmp", + permissionGate: gate, + }); const ripgrepIndex = findMiddlewareIndex(plugins, "no matches for /"); expect(ripgrepIndex).toBeGreaterThanOrEqual(0); plugins[ripgrepIndex] = shortCircuitingPlugin; diff --git a/src/agent/posix-tool-plugins.ts b/src/agent/posix-tool-plugins.ts index 905134738..2d8d39cf8 100644 --- a/src/agent/posix-tool-plugins.ts +++ b/src/agent/posix-tool-plugins.ts @@ -16,7 +16,10 @@ import { type SpillBlobWriter, } from "../plugins/result-truncation-plugin.js"; import { toolResultSecretScrubPlugin } from "../plugins/tool-result-secret-scrub-plugin.js"; -import { shellGuardPlugin, type ShellTimeoutConfig } from "../plugins/shell-guard-plugin.js"; +import { + shellGuardPlugin, + type ShellTimeoutConfig, +} from "../plugins/shell-guard-plugin.js"; import type { BackgroundShellRegistry } from "../shell/background-shell.js"; import { readFileGuardPlugin, @@ -65,7 +68,9 @@ export interface CorePosixToolPluginsArgs { // the fragment, and a bare, unredacted piece of the credential reaches the // model with no redaction marker. Scrub-then-truncate is always safe, since // truncating already-redacted text loses nothing sensitive. -export function buildCorePosixToolPlugins(args: CorePosixToolPluginsArgs): ToolPlugin[] { +export function buildCorePosixToolPlugins( + args: CorePosixToolPluginsArgs, +): ToolPlugin[] { const { cwd, permissionGate, @@ -101,7 +106,9 @@ export function buildCorePosixToolPlugins(args: CorePosixToolPluginsArgs): ToolP permissionPlugin(permissionGate), shellGuardPlugin(cwd, shellTimeout, shellEnv, { allowOutsideCwd: allowOutside, - ...(getBackgroundShellRegistry !== undefined ? { getBackgroundShellRegistry } : {}), + ...(getBackgroundShellRegistry !== undefined + ? { getBackgroundShellRegistry } + : {}), }), readFileGuardPlugin(cwd, readFileGuard), ripgrepPlugin(cwd), diff --git a/src/agent/product-mutation-tools.test.ts b/src/agent/product-mutation-tools.test.ts index 66cacd9ba..00dac8bbb 100644 --- a/src/agent/product-mutation-tools.test.ts +++ b/src/agent/product-mutation-tools.test.ts @@ -32,11 +32,16 @@ describe("PRODUCT_MUTATION_TOOLS", () => { +new *** End Patch `; - expect(productMutationPaths("apply_patch", { input })).toEqual(["hello.txt", "src/app.py"]); + expect(productMutationPaths("apply_patch", { input })).toEqual([ + "hello.txt", + "src/app.py", + ]); }); test("productMutationPaths returns [] for malformed apply_patch input", () => { - expect(productMutationPaths("apply_patch", { input: "not a patch" })).toEqual([]); + expect( + productMutationPaths("apply_patch", { input: "not a patch" }), + ).toEqual([]); expect(productMutationPaths("apply_patch", {})).toEqual([]); }); @@ -46,7 +51,11 @@ describe("PRODUCT_MUTATION_TOOLS", () => { +x *** End Patch `; - const reqs = buildRequests({ id: "c", name: "apply_patch", arguments: { input } }); + const reqs = buildRequests({ + id: "c", + name: "apply_patch", + arguments: { input }, + }); expect(reqs).toHaveLength(1); expect(reqs[0]?.tool).toBe("apply_patch"); expect(reqs[0]?.subject).toBe("src/a.ts"); diff --git a/src/agent/product-mutation-tools.ts b/src/agent/product-mutation-tools.ts index 8271d2a5a..580b8d2e9 100644 --- a/src/agent/product-mutation-tools.ts +++ b/src/agent/product-mutation-tools.ts @@ -21,7 +21,9 @@ export const PRODUCT_MUTATION_TOOLS = [ export type ProductMutationToolName = (typeof PRODUCT_MUTATION_TOOLS)[number]; -const PRODUCT_MUTATION_TOOL_SET: ReadonlySet = new Set(PRODUCT_MUTATION_TOOLS); +const PRODUCT_MUTATION_TOOL_SET: ReadonlySet = new Set( + PRODUCT_MUTATION_TOOLS, +); export function isProductMutationTool(name: string): boolean { return PRODUCT_MUTATION_TOOL_SET.has(name); diff --git a/src/agent/profiles.ts b/src/agent/profiles.ts index 82a37a4a7..680f62d3a 100644 --- a/src/agent/profiles.ts +++ b/src/agent/profiles.ts @@ -30,8 +30,12 @@ const CapabilityFilterSchema = type({ // is statically typed for literal strings; a computed string requires a cast // through `unknown`. The schema is exercised by tests/unit/data-only-agent // and the runtime ReasoningEffort re-export, so drift is caught. -const reasoningEffortLiteral = REASONING_EFFORTS.map((e) => `'${e}'`).join(" | "); -const ReasoningEffortSchema = type(reasoningEffortLiteral as unknown as "'none'"); +const reasoningEffortLiteral = REASONING_EFFORTS.map((e) => `'${e}'`).join( + " | ", +); +const ReasoningEffortSchema = type( + reasoningEffortLiteral as unknown as "'none'", +); const InferenceLegSchema = type({ provider: "string>0", @@ -133,9 +137,15 @@ export async function loadAgentProfiles( if (isReservedDirectorProfile(profile)) continue; // Resolve systemPromptPath relative to this directory. The file content // becomes systemPromptRole; an explicit systemPromptRole takes precedence. - if (profile.systemPromptPath !== undefined && profile.systemPromptRole === undefined) { + if ( + profile.systemPromptPath !== undefined && + profile.systemPromptRole === undefined + ) { try { - const promptRaw = await readFile(join(dir, profile.systemPromptPath), "utf8"); + const promptRaw = await readFile( + join(dir, profile.systemPromptPath), + "utf8", + ); profile.systemPromptRole = promptRaw.trim(); } catch { // Missing prompt file is non-fatal — the profile loads without a role. diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index 1d9d521df..05db7d7e6 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -10,7 +10,10 @@ import { CORE_TOOL_NAMES, CATALOG_TOOL_NAMES } from "./tool-search.js"; // Tool names referenced in the discipline block must exist in the actual // registration source, not be assumed. web_fetch/web_search are catalog tools // (always advertised) and also registered via createWebFetchTool/createWebSearchTool. -const REGISTERED_TOOL_NAMES = new Set([...CORE_TOOL_NAMES, ...CATALOG_TOOL_NAMES]); +const REGISTERED_TOOL_NAMES = new Set([ + ...CORE_TOOL_NAMES, + ...CATALOG_TOOL_NAMES, +]); const REFERENCED_TOOL_NAMES = [ "read_file", @@ -29,7 +32,9 @@ function expectVerificationGuidance(prompt: string): void { expect(prompt).toMatch( /defined typecheck command.*relevant tests.*defined full verification command/is, ); - expect(prompt).toMatch(/repository defines no typecheck command.*explicit Blocker/is); + expect(prompt).toMatch( + /repository defines no typecheck command.*explicit Blocker/is, + ); expect(prompt).toMatch(/evidence.*AGENTS.*package scripts/is); expect(prompt).toMatch(/do not invent.*typecheck command/i); expect(prompt).toMatch(/exact verification command.*outcome.*exit status/is); @@ -63,7 +68,9 @@ describe("buildPromptDisciplineBlock", () => { expect(block).toContain("cat/head/tail"); expect(block).toContain("heredoc/echo"); // Environment. - expect(block).toMatch(/never set, export, or prefix environment variables/i); + expect(block).toMatch( + /never set, export, or prefix environment variables/i, + ); expect(block).toMatch(/project settings/i); // Web. expect(block).toMatch(/curl or wget/i); @@ -86,7 +93,13 @@ describe("buildPromptDisciplineBlock", () => { describe("shared discipline block appears exactly once per built prompt", () => { it("appears exactly once in the orchestrator chat prompt", () => { - const prompt = buildChatSystemPrompt(undefined, undefined, undefined, [], "orchestrator"); + const prompt = buildChatSystemPrompt( + undefined, + undefined, + undefined, + [], + "orchestrator", + ); expect(countOccurrences(prompt, "Prompt discipline:")).toBe(1); }); @@ -125,7 +138,13 @@ describe("shared verification guidance", () => { }); it("requires evidence-carrying verification in orchestrator chat prompts", () => { - const prompt = buildChatSystemPrompt(undefined, undefined, undefined, [], "orchestrator"); + const prompt = buildChatSystemPrompt( + undefined, + undefined, + undefined, + [], + "orchestrator", + ); expectVerificationGuidance(prompt); }); }); diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index e7369845b..749cfc4e4 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -40,7 +40,9 @@ function formatDateDDMMYYYY(date: Date): string { return `${day}/${month}/${year}`; } -export function buildChatRole(_sessionMode: SessionMode = "orchestrator"): string { +export function buildChatRole( + _sessionMode: SessionMode = "orchestrator", +): string { // Primary session identity is the closed Skywalker director package (CL-5817). // Harness facts / guidelines still append after this role in baseSection. return createSkywalkerSystemPrompt(); @@ -106,7 +108,11 @@ export function buildHarnessFacts( } export function buildGuidelines( - opts: { subAgent?: boolean; sessionMode?: SessionMode; askDirector?: boolean } = {}, + opts: { + subAgent?: boolean; + sessionMode?: SessionMode; + askDirector?: boolean; + } = {}, ): string { const subAgent = opts.subAgent ?? false; const askDirector = opts.askDirector === true; @@ -181,7 +187,9 @@ export function buildGuidelines( // appended exactly once per built prompt. Prohibition form throughout: these // are the failure modes observed across shipped agents (OpenCode, Codex CLI, // Gemini CLI, Claude Code, Warp, Aider, Cline), not general advice. -export function buildPromptDisciplineBlock(opts: { subAgent?: boolean } = {}): string { +export function buildPromptDisciplineBlock( + opts: { subAgent?: boolean } = {}, +): string { const subAgent = opts.subAgent ?? false; const toolsOverShell = subAgent ? "- Never use run_shell to read, edit, or write files — use read_file, edit_file, write_file; cat/head/tail, sed/awk/perl -i, and heredoc/echo redirection are prohibited substitutes." @@ -238,26 +246,36 @@ const TOOL_SUMMARIES: Record = { "wait for spawned workers by agent_id; returns awaiting_director when a worker asks, without collecting that session", search_agents: "find agent profiles by role or team before spawning with spawn_agent(agent=...); results include full system prompt / body so you need not read_file plugin roots outside the workspace", - manage_tasks: "maintain your work checklist — create/replace, update status, append, cancel", + manage_tasks: + "maintain your work checklist — create/replace, update status, append, cancel", ask_director: "pause and ask the spawning parent (not the human) a short clarifying question with short option labels; parent answers via send_input; after the cap, proceed with best judgment or put remaining questions in Blockers", - submit_output: "signal the task is complete, or complete a workflow step by passing its step id", + submit_output: + "signal the task is complete, or complete a workflow step by passing its step id", ask_operator: "pause and ask the user when blocked or genuinely ambiguous; put long rationale in a transcript reply first, then call with a short question and short option labels only", present: "dynamically render aligned/structured output using the layout primitives (stack/row/grid/text etc)", tool_search: "load more tools by capability when you need them", - use_skill: "load a listed skill's full instructions before doing work it covers", + use_skill: + "load a listed skill's full instructions before doing work it covers", skill_search: "look up skill descriptions by capability (catalog — call directly, do not tool_search for this)", }; -export function buildAvailableTools(tools: readonly string[] = CORE_TOOL_NAMES): string { - const lines = tools.map((tool) => `- ${tool}: ${TOOL_SUMMARIES[tool] ?? "available"}`); +export function buildAvailableTools( + tools: readonly string[] = CORE_TOOL_NAMES, +): string { + const lines = tools.map( + (tool) => `- ${tool}: ${TOOL_SUMMARIES[tool] ?? "available"}`, + ); return ["Tools:", ...lines].join("\n"); } -export function buildActiveContext(date = new Date(), cwd = process.cwd()): string { +export function buildActiveContext( + date = new Date(), + cwd = process.cwd(), +): string { return [ "Active context:", `Current Date: ${formatDateDDMMYYYY(date)} (prompt cache survives for <=24hr)`, @@ -281,7 +299,9 @@ export function buildEnvironmentContext(env: EnvironmentInfo): string { if (!env.isGitRepo) { lines.push("Git: not a git repository"); } else if ((env.gitDirtyCount ?? 0) === 0) { - lines.push(`Git: on ${env.gitBranch ?? "(detached HEAD)"}, working tree clean`); + lines.push( + `Git: on ${env.gitBranch ?? "(detached HEAD)"}, working tree clean`, + ); } else { lines.push( `Git: on ${env.gitBranch ?? "(detached HEAD)"}, ${env.gitDirtyCount} uncommitted change(s):`, @@ -301,7 +321,10 @@ function contextSection(env?: EnvironmentInfo): string { // The static base — role, harness facts, guidelines. A SYSTEM.md override // keeps custom text but still appends mode-specific harness + guidelines; tools, // env, and appended extensions attach after that. -function baseSection(baseOverride: string | undefined, sessionMode: SessionMode): string { +function baseSection( + baseOverride: string | undefined, + sessionMode: SessionMode, +): string { if (baseOverride !== undefined && baseOverride.trim().length > 0) { const custom = baseOverride.trim(); // SYSTEM.md can describe the role; orchestrator harness rules always apply on the wire. @@ -340,7 +363,9 @@ export function buildChatSystemPrompt( ): string { const sections = [ baseSection(baseOverride, sessionMode), - buildAvailableTools(coreToolNamesForSessionMode(sessionMode, toolAvailability)), + buildAvailableTools( + coreToolNamesForSessionMode(sessionMode, toolAvailability), + ), ]; if (skills.length > 0) sections.push(buildSkillsSection(skills)); sections.push(contextSection(env)); @@ -391,7 +416,9 @@ export function buildSubAgentAppendix( // Final-reply envelope the parent can parse. Free-form prose is allowed inside // each field; the headings are the structure. When the brief carries Success // criteria / Do not, those are the completion gate and scope fence. -export function buildSubAgentReportContract(opts: { askDirector?: boolean } = {}): string { +export function buildSubAgentReportContract( + opts: { askDirector?: boolean } = {}, +): string { const askDirector = opts.askDirector === true; return [ "Reporting back:", @@ -443,19 +470,29 @@ export function buildSubAgentSystemPrompt( } = {}, ): string { const toolListForPrompt = - opts.toolNames && opts.toolNames.length > 0 ? opts.toolNames : defaultChatTools; + opts.toolNames && opts.toolNames.length > 0 + ? opts.toolNames + : defaultChatTools; const askDirector = toolListForPrompt.includes("ask_director"); const base = baseOverride !== undefined && baseOverride.trim().length > 0 ? baseOverride.trim() : joinSections([ `You are a fleet agent — a worker dispatched by ${PRODUCT_NAME} to carry out one self-contained job autonomously. You have the full file, search, and shell toolset under the same permission policy as the parent session (saved grants and auto mode when eligible; operator approval otherwise). Finish the job and report back. Your manage_tasks checklist (if you use it) is yours alone; it is not shared with the parent.`, - buildHarnessFacts({ dynamicTools: false, subAgent: true, askDirector }), + buildHarnessFacts({ + dynamicTools: false, + subAgent: true, + askDirector, + }), buildGuidelines({ subAgent: true, askDirector }), buildPromptDisciplineBlock({ subAgent: true }), buildSubAgentReportContract({ askDirector }), ]); - const sections = [base, buildAvailableTools(toolListForPrompt), contextSection(env)]; + const sections = [ + base, + buildAvailableTools(toolListForPrompt), + contextSection(env), + ]; if (extensions !== undefined && extensions.length > 0) { sections.push(...extensions); } diff --git a/src/agent/reactor-events.test.ts b/src/agent/reactor-events.test.ts index 15612e63e..d2eedf901 100644 --- a/src/agent/reactor-events.test.ts +++ b/src/agent/reactor-events.test.ts @@ -49,7 +49,9 @@ const emittedEvents: ReactorEmittedEvent[] = [ { type: "message.received", seq: 0, - data: { message: { role: "user", content: [{ type: "text", text: "hi" }] } }, + data: { + message: { role: "user", content: [{ type: "text", text: "hi" }] }, + }, } as unknown as ReactorEmittedEvent, { type: "inference.done", data: {} } as unknown as ReactorEmittedEvent, { type: "reactor.done", data: {} } as unknown as ReactorEmittedEvent, diff --git a/src/agent/reactor-events.ts b/src/agent/reactor-events.ts index 140392a7b..eaa2ec548 100644 --- a/src/agent/reactor-events.ts +++ b/src/agent/reactor-events.ts @@ -16,9 +16,11 @@ /** True when `event` is the turn boundary — fires once per turn, every turn. */ export const onTurnBoundary = ( event: E, -): event is Extract => event.type === "inference.done"; +): event is Extract => + event.type === "inference.done"; /** True when `event` is reactor shutdown — fires once, at the end of the run. */ export const onReactorShutdown = ( event: E, -): event is Extract => event.type === "reactor.done"; +): event is Extract => + event.type === "reactor.done"; diff --git a/src/agent/renderer.ts b/src/agent/renderer.ts index 15fdb0bcd..9759ae158 100644 --- a/src/agent/renderer.ts +++ b/src/agent/renderer.ts @@ -68,11 +68,14 @@ function formatOp(name: string): string { return name; } -function tokenUsageFromEvent(data: Record | undefined): TokenUsage | null { +function tokenUsageFromEvent( + data: Record | undefined, +): TokenUsage | null { const usage = data?.usage; if (usage === null || typeof usage !== "object") return null; const fields = usage as Record; - if (typeof fields.input !== "number" || typeof fields.output !== "number") return null; + if (typeof fields.input !== "number" || typeof fields.output !== "number") + return null; return { input: fields.input, output: fields.output, @@ -152,7 +155,11 @@ export function createRenderer( ); } - function writeShellBlock(command: string, output: string, isError: boolean): void { + function writeShellBlock( + command: string, + output: string, + isError: boolean, + ): void { const status = isError ? `${RED}✗${RESET}` : `${GREEN}✓${RESET}`; if (isError) { process.stdout.write( @@ -174,14 +181,21 @@ export function createRenderer( } function render(event: ReactorEmittedEvent): void { - const e = event as { type: string; seq?: number; data?: Record }; + const e = event as { + type: string; + seq?: number; + data?: Record; + }; switch (e.type) { case "inference.tool_call.start": { const name = String(e.data?.name ?? ""); currentOp = formatOp(name); currentArg = - name === "read_file" || name === "list_dir" || name === "search_files" || name === "grep" + name === "read_file" || + name === "list_dir" || + name === "search_files" || + name === "grep" ? String((e.data as Record).callId ?? "") : ""; break; @@ -206,13 +220,18 @@ export function createRenderer( currentArg = ""; const usage = tokenUsageFromEvent(e.data); if (usage !== null) { - sessionCost.addTurn(usage, billingIdentityFromEvent(e.data, modelId ?? "")); + sessionCost.addTurn( + usage, + billingIdentityFromEvent(e.data, modelId ?? ""), + ); } break; } case "tool.start": { - const callName = String((e.data?.call as Record)?.name ?? ""); + const callName = String( + (e.data?.call as Record)?.name ?? "", + ); currentOp = formatOp(callName); currentArg = ""; break; @@ -260,15 +279,21 @@ export function createRenderer( case "inference.error": { const err = e.data?.error as Record | undefined; - const rawMessage = String(err?.message ?? e.data?.error ?? "inference error"); + const rawMessage = String( + err?.message ?? e.data?.error ?? "inference error", + ); const classifiedError = typeof err?.category === "string" ? { category: err.category, message: rawMessage, - ...(typeof err.statusCode === "number" ? { statusCode: err.statusCode } : {}), + ...(typeof err.statusCode === "number" + ? { statusCode: err.statusCode } + : {}), ...(err.raw !== undefined ? { raw: err.raw } : {}), - ...(typeof err.providerId === "string" ? { providerId: err.providerId } : {}), + ...(typeof err.providerId === "string" + ? { providerId: err.providerId } + : {}), } : undefined; const message = diff --git a/src/agent/retry-policy.test.ts b/src/agent/retry-policy.test.ts index 0feaed8d4..bbf0699b2 100644 --- a/src/agent/retry-policy.test.ts +++ b/src/agent/retry-policy.test.ts @@ -1,15 +1,18 @@ import { describe, expect, test } from "bun:test"; import type { AdmissionQueue } from "../subagent/admission.js"; -import { createCorbitsRetryPolicy, type CorbitsRetryPolicyOptions } from "./retry-policy.js"; +import { + createCorbitsRetryPolicy, + type CorbitsRetryPolicyOptions, +} from "./retry-policy.js"; const HTML_503 = `503 Service Unavailable Cloudflare`; const silentAdmission: AdmissionQueue = { enqueue: () => "running", - release: () => {}, - setCapacity: () => {}, - notePressure: () => {}, - cancel: () => {}, + release: () => undefined, + setCapacity: () => undefined, + notePressure: () => undefined, + cancel: () => undefined, occupied: () => false, }; @@ -209,7 +212,10 @@ describe("createCorbitsRetryPolicy", () => { error: { category: "retryable" as const, message: "gateway timeout" }, }); expect(await decide(situation(1))).toEqual({ kind: "retry", delayMs: 500 }); - expect(await decide(situation(2))).toEqual({ kind: "retry", delayMs: 1000 }); + expect(await decide(situation(2))).toEqual({ + kind: "retry", + delayMs: 1000, + }); expect(await decide(situation(3))).toEqual({ kind: "abort" }); }); @@ -225,7 +231,10 @@ describe("createCorbitsRetryPolicy", () => { }, }); expect(await decide(situation(1))).toEqual({ kind: "retry", delayMs: 500 }); - expect(await decide(situation(2))).toEqual({ kind: "retry", delayMs: 1000 }); + expect(await decide(situation(2))).toEqual({ + kind: "retry", + delayMs: 1000, + }); expect(await decide(situation(3))).toEqual({ kind: "abort" }); }); @@ -252,12 +261,12 @@ describe("createCorbitsRetryPolicy", () => { const notes: { provider: string; until: number }[] = []; const admission: AdmissionQueue = { enqueue: () => "running", - release: () => {}, - setCapacity: () => {}, + release: () => undefined, + setCapacity: () => undefined, notePressure: (provider: string, untilMs: number) => { notes.push({ provider, until: untilMs }); }, - cancel: () => {}, + cancel: () => undefined, occupied: () => false, }; const decide = policy({ @@ -312,8 +321,14 @@ describe("createCorbitsRetryPolicy", () => { retryAfterMs: 5_000, }, }); - expect(await decide(situation(1))).toEqual({ kind: "retry", delayMs: 5_000 }); - expect(await decide(situation(2))).toEqual({ kind: "retry", delayMs: 5_000 }); + expect(await decide(situation(1))).toEqual({ + kind: "retry", + delayMs: 5_000, + }); + expect(await decide(situation(2))).toEqual({ + kind: "retry", + delayMs: 5_000, + }); expect(await decide(situation(3))).toEqual({ kind: "abort" }); }); @@ -352,10 +367,17 @@ describe("createCorbitsRetryPolicy", () => { const situation = (attempt: number) => ({ attempt, elapsedMs: 0, - error: { category: "retryable" as const, message: "boom", statusCode: 429 }, + error: { + category: "retryable" as const, + message: "boom", + statusCode: 429, + }, }); expect(await decide(situation(1))).toEqual({ kind: "retry", delayMs: 500 }); - expect(await decide(situation(2))).toEqual({ kind: "retry", delayMs: 1000 }); + expect(await decide(situation(2))).toEqual({ + kind: "retry", + delayMs: 1000, + }); expect(await decide(situation(3))).toEqual({ kind: "abort" }); }); }); diff --git a/src/agent/retry-policy.ts b/src/agent/retry-policy.ts index 093126542..1c517f349 100644 --- a/src/agent/retry-policy.ts +++ b/src/agent/retry-policy.ts @@ -1,10 +1,17 @@ import { createDefaultRetryPolicy } from "@intx/inference"; -import type { RetryDecision, RetryPolicy, RetrySituation } from "@intx/types/runtime"; +import type { + RetryDecision, + RetryPolicy, + RetrySituation, +} from "@intx/types/runtime"; import { normalizeInferenceErrorForRetry, type InferenceErrorWithGoContext, } from "../inference-gateway-error.js"; -import { getProcessAdmissionQueue, type AdmissionQueue } from "../subagent/admission.js"; +import { + getProcessAdmissionQueue, + type AdmissionQueue, +} from "../subagent/admission.js"; // Providers that enforce long-window quotas (e.g. monthly limits) set // Retry-After to days or weeks. The default policy trusts that value and @@ -33,11 +40,15 @@ export interface CorbitsRetryPolicyOptions { * short 429 → retryable, Go, Codex) can gate on context the harness does not * attach to InferenceError today. */ -export function createCorbitsRetryPolicy(options?: CorbitsRetryPolicyOptions): RetryPolicy { +export function createCorbitsRetryPolicy( + options?: CorbitsRetryPolicyOptions, +): RetryPolicy { const defaultPolicy = createDefaultRetryPolicy(); const admission = options?.admission ?? getProcessAdmissionQueue(); const now = options?.now ?? Date.now; - return (situation: RetrySituation): RetryDecision | Promise => { + return ( + situation: RetrySituation, + ): RetryDecision | Promise => { const raw = options?.providerId; const stampedProviderId = typeof raw === "function" ? raw() : raw; const incoming = situation.error as InferenceErrorWithGoContext; @@ -47,8 +58,12 @@ export function createCorbitsRetryPolicy(options?: CorbitsRetryPolicyOptions): R : incoming; const error = normalizeInferenceErrorForRetry(withProvider); if (error.category === "retryable" && error.statusCode === 429) { - const pauseMs = Math.min(error.retryAfterMs ?? DEFAULT_PRESSURE_PAUSE_MS, MAX_BLIND_WAIT_MS); - const provider = withProvider.providerId ?? stampedProviderId ?? "unknown"; + const pauseMs = Math.min( + error.retryAfterMs ?? DEFAULT_PRESSURE_PAUSE_MS, + MAX_BLIND_WAIT_MS, + ); + const provider = + withProvider.providerId ?? stampedProviderId ?? "unknown"; admission.notePressure(provider, now() + pauseMs); // The vendored default retries `retryable` on a fixed 500/1000ms // schedule and ignores Retry-After. A 429 carries the server's pacing @@ -61,7 +76,9 @@ export function createCorbitsRetryPolicy(options?: CorbitsRetryPolicyOptions): R if (error.retryAfterMs >= RATE_LIMIT_HANG_MS) return { kind: "abort" }; const retryAfterMs = error.retryAfterMs; const honorRetryAfter = (decision: RetryDecision): RetryDecision => - decision.kind === "retry" ? { kind: "retry", delayMs: retryAfterMs } : decision; + decision.kind === "retry" + ? { kind: "retry", delayMs: retryAfterMs } + : decision; const decision = defaultPolicy({ ...situation, error }); if (decision instanceof Promise) return decision.then(honorRetryAfter); return honorRetryAfter(decision); diff --git a/src/agent/skill-search.test.ts b/src/agent/skill-search.test.ts index 62b2bd576..83544a091 100644 --- a/src/agent/skill-search.test.ts +++ b/src/agent/skill-search.test.ts @@ -3,7 +3,10 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "bun:test"; -import { createSkillSearchTool, skillSearchDefinition } from "./skill-search.js"; +import { + createSkillSearchTool, + skillSearchDefinition, +} from "./skill-search.js"; import type { SkillSummary } from "../extensions/skills.js"; function call( @@ -26,7 +29,9 @@ describe("skillSearchDefinition", () => { expect(skillSearchDefinition.description).toContain("system prompt"); expect(skillSearchDefinition.description).toContain("use_skill"); expect(skillSearchDefinition.description).toMatch(/directly callable/i); - expect(skillSearchDefinition.description).not.toMatch(/find this via tool_search/i); + expect(skillSearchDefinition.description).not.toMatch( + /find this via tool_search/i, + ); }); }); diff --git a/src/agent/skill-search.ts b/src/agent/skill-search.ts index c32f7a6ae..f4ab7f02d 100644 --- a/src/agent/skill-search.ts +++ b/src/agent/skill-search.ts @@ -46,7 +46,11 @@ function visibleSkills( return skills.filter((skill) => allowed.has(skill.name)); } -function scoreSkill(skill: SkillSummary, queryTokens: string[], rawQuery: string): number { +function scoreSkill( + skill: SkillSummary, + queryTokens: string[], + rawQuery: string, +): number { const nameTokens = tokenize(skill.name); const descTokens = new Set(tokenize(skill.description)); let total = 0; @@ -64,7 +68,9 @@ const SkillSearchArgs = type({ query: "string" }); const DEFAULT_LIMIT = 8; -export function createSkillSearchTool(args: CreateSkillSearchToolArgs): AgentTool { +export function createSkillSearchTool( + args: CreateSkillSearchToolArgs, +): AgentTool { const catalog = visibleSkills(args.skills, args.allowedNames); return stringTool({ definition: skillSearchDefinition, @@ -74,14 +80,18 @@ export function createSkillSearchTool(args: CreateSkillSearchToolArgs): AgentToo return "Error: skill_search requires query (string)."; } const query = parsed.query.trim(); - if (query.length === 0) return "Error: skill_search requires a non-empty query."; + if (query.length === 0) + return "Error: skill_search requires a non-empty query."; const rawQuery = query.toLowerCase(); const queryTokens = tokenize(query); if (queryTokens.length === 0) { return `No skills matched "${query}". Try different keywords describing the capability.`; } const matches = catalog - .map((skill) => ({ skill, score: scoreSkill(skill, queryTokens, rawQuery) })) + .map((skill) => ({ + skill, + score: scoreSkill(skill, queryTokens, rawQuery), + })) .filter((entry) => entry.score > 0) .sort((a, b) => b.score - a.score) .slice(0, DEFAULT_LIMIT) @@ -89,7 +99,9 @@ export function createSkillSearchTool(args: CreateSkillSearchToolArgs): AgentToo if (matches.length === 0) { return `No skills matched "${query}". Try different keywords describing the capability.`; } - return matches.map((skill) => `- ${skill.name}: ${skill.description}`).join("\n"); + return matches + .map((skill) => `- ${skill.name}: ${skill.description}`) + .join("\n"); }, }); } diff --git a/src/agent/tasks.ts b/src/agent/tasks.ts index 16fe2bce0..2b919a37c 100644 --- a/src/agent/tasks.ts +++ b/src/agent/tasks.ts @@ -58,7 +58,8 @@ export const manageTasksDefinition: ToolDefinition = { }, tasks: { type: "array", - description: 'For action="create": the new ordered task list (full replace).', + description: + 'For action="create": the new ordered task list (full replace).', items: { type: "object", properties: { @@ -66,7 +67,10 @@ export const manageTasksDefinition: ToolDefinition = { type: "string", description: "Stable id, unique within this list (e.g. t1, t2).", }, - title: { type: "string", description: "Short, action-oriented description." }, + title: { + type: "string", + description: "Short, action-oriented description.", + }, status: { type: "string", enum: ["todo", "doing", "done", "cancelled"], @@ -84,10 +88,14 @@ export const manageTasksDefinition: ToolDefinition = { items: { type: "object", properties: { - id: { type: "string", description: "Id of an existing task, or a new id to append." }, + id: { + type: "string", + description: "Id of an existing task, or a new id to append.", + }, title: { type: "string", - description: "Required when appending a new id; optional rename for existing.", + description: + "Required when appending a new id; optional rename for existing.", }, status: { type: "string", @@ -110,10 +118,17 @@ export function parseManageTasksArgs(rawArgs: unknown): ManageTasksArgs | null { // Apply a parsed call to a task list, returning the full list. Completed tasks // are retained so the task view can show them checked off as work progresses. -export function applyManageTasks(current: Task[], args: ManageTasksArgs): Task[] { +export function applyManageTasks( + current: Task[], + args: ManageTasksArgs, +): Task[] { if (args.action === "create") { const tasks = args.tasks ?? []; - return tasks.map((t) => ({ id: t.id, title: t.title, status: t.status ?? "todo" })); + return tasks.map((t) => ({ + id: t.id, + title: t.title, + status: t.status ?? "todo", + })); } const updates = args.updates ?? []; if (updates.length === 0) return current; diff --git a/src/agent/tool-classification.test.ts b/src/agent/tool-classification.test.ts index 82e9106f0..1325c44a7 100644 --- a/src/agent/tool-classification.test.ts +++ b/src/agent/tool-classification.test.ts @@ -11,7 +11,14 @@ import { describe("AUTO_ALLOW_READ_TOOLS", () => { test("gates auto-allow with exactly this membership", () => { expect([...AUTO_ALLOW_READ_TOOLS].sort()).toEqual( - ["grep", "list_dir", "lsp", "manage_tasks", "read_file", "search_files"].sort(), + [ + "grep", + "list_dir", + "lsp", + "manage_tasks", + "read_file", + "search_files", + ].sort(), ); }); diff --git a/src/agent/tool-classification.ts b/src/agent/tool-classification.ts index 628120b33..161c6852f 100644 --- a/src/agent/tool-classification.ts +++ b/src/agent/tool-classification.ts @@ -29,7 +29,9 @@ import { READ_TOOLS as DIRECTOR_READ_TOOLS } from "./directors/tool-sets.js"; * compaction's re-read dedup and thrash's read-count bookkeeping — both are * asking the same question ("was this path already read?"). */ -export const PATH_KEYED_READ_TOOLS: ReadonlySet = new Set(["read_file"]); +export const PATH_KEYED_READ_TOOLS: ReadonlySet = new Set([ + "read_file", +]); /** * grep / search_files: pattern-keyed query tools whose repeated identical @@ -37,7 +39,10 @@ export const PATH_KEYED_READ_TOOLS: ReadonlySet = new Set(["read_file"]) * both compaction and thrash build on; each adds/omits list_dir for its own * reason (see compactor.ts's QUERY_TOOLS and thrash.ts's SEARCH_TOOLS). */ -export const SEARCH_QUERY_TOOLS: ReadonlySet = new Set(["grep", "search_files"]); +export const SEARCH_QUERY_TOOLS: ReadonlySet = new Set([ + "grep", + "search_files", +]); /** * Tools that never need an approval prompt because they cannot change the diff --git a/src/agent/tool-schema-normalize.test.ts b/src/agent/tool-schema-normalize.test.ts index becf2e919..3ea859b31 100644 --- a/src/agent/tool-schema-normalize.test.ts +++ b/src/agent/tool-schema-normalize.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { presentDefinition } from "./director.js"; import { manageTasksDefinition } from "./tasks.js"; @@ -39,9 +40,12 @@ describe("normalizeToolDefinitionsForProvider", () => { test("present description and kimi view description share primitives guidance (no dual prose drift)", () => { expect(PRESENT_VIEW_PRIMITIVES_GUIDANCE.length).toBeGreaterThan(0); - expect(presentDefinition.description).toContain(PRESENT_VIEW_PRIMITIVES_GUIDANCE); - const viewDesc = (KIMI_PRESENT_INPUT_SCHEMA.properties.view as { description: string }) - .description; + expect(presentDefinition.description).toContain( + PRESENT_VIEW_PRIMITIVES_GUIDANCE, + ); + const viewDesc = ( + KIMI_PRESENT_INPUT_SCHEMA.properties.view as { description: string } + ).description; expect(viewDesc).toContain(PRESENT_VIEW_PRIMITIVES_GUIDANCE); }); @@ -52,9 +56,9 @@ describe("normalizeToolDefinitionsForProvider", () => { }); const present = out.find((d) => d.name === "present"); expect(present).toBeDefined(); - expect(schemaHasRef(present!.inputSchema)).toBe(false); - expect(schemaHasDefs(present!.inputSchema)).toBe(false); - const schema = present!.inputSchema as { + expect(schemaHasRef(defined(present).inputSchema)).toBe(false); + expect(schemaHasDefs(defined(present).inputSchema)).toBe(false); + const schema = defined(present).inputSchema as { type?: string; required?: string[]; properties?: { @@ -68,11 +72,13 @@ describe("normalizeToolDefinitionsForProvider", () => { expect(schema.required).toEqual(["view"]); // Richer non-recursive shape: view is oneOf of primitives, not bare freeform. expect(Array.isArray(schema.properties?.view?.oneOf)).toBe(true); - expect((schema.properties?.view?.oneOf ?? []).length).toBeGreaterThanOrEqual(4); + expect( + (schema.properties?.view?.oneOf ?? []).length, + ).toBeGreaterThanOrEqual(4); expect(schema.properties?.view?.description).toContain("Primitives:"); // Description + examples stay on the tool for model guidance. - expect(present!.description).toBe(recursivePresent.description); - expect(present!.description.length).toBeGreaterThan(0); + expect(defined(present).description).toBe(recursivePresent.description); + expect(defined(present).description.length).toBeGreaterThan(0); }); test("kimi advertise payload is the exact Moonshot wire shape (pinned fixture, no live Moonshot)", () => { @@ -83,12 +89,14 @@ describe("normalizeToolDefinitionsForProvider", () => { providerName: "moonshot", model: "kimi-k2", }); - const present = out.find((d) => d.name === "present")!; + const present = defined(out.find((d) => d.name === "present")); expect(present.inputSchema).toEqual( structuredClone(KIMI_PRESENT_INPUT_SCHEMA) as typeof present.inputSchema, ); // Stable JSON pin of the full wire schema object. - expect(JSON.stringify(present.inputSchema)).toBe(JSON.stringify(KIMI_PRESENT_INPUT_SCHEMA)); + expect(JSON.stringify(present.inputSchema)).toBe( + JSON.stringify(KIMI_PRESENT_INPUT_SCHEMA), + ); }); test("opencode-go + kimi-k3 rewrites present (model-id gate)", () => { @@ -96,7 +104,7 @@ describe("normalizeToolDefinitionsForProvider", () => { providerName: "opencode-go", model: "kimi-k3", }); - const present = out.find((d) => d.name === "present")!; + const present = defined(out.find((d) => d.name === "present")); expect(schemaHasRef(present.inputSchema)).toBe(false); expect(schemaHasDefs(present.inputSchema)).toBe(false); }); @@ -106,7 +114,9 @@ describe("normalizeToolDefinitionsForProvider", () => { providerName: "openai-compat", model: "kimi-k3", }); - expect(schemaHasRef(out.find((d) => d.name === "present")!.inputSchema)).toBe(false); + expect( + schemaHasRef(defined(out.find((d) => d.name === "present")).inputSchema), + ).toBe(false); }); test("non-kimi providers get identity schemas (recursive present kept)", () => { @@ -118,7 +128,7 @@ describe("normalizeToolDefinitionsForProvider", () => { ] as const) { const out = normalizeToolDefinitionsForProvider(defs, ctx); expect(out).toBe(defs); - const present = out.find((d) => d.name === "present")!; + const present = defined(out.find((d) => d.name === "present")); expect(schemaHasRef(present.inputSchema)).toBe(true); expect(present.inputSchema).toBe(recursivePresent.inputSchema); } diff --git a/src/agent/tool-schema-normalize.ts b/src/agent/tool-schema-normalize.ts index 0ac3a9ffd..1306efd99 100644 --- a/src/agent/tool-schema-normalize.ts +++ b/src/agent/tool-schema-normalize.ts @@ -20,7 +20,14 @@ export const PRESENT_VIEW_PRIMITIVES_GUIDANCE = "tone is one of default|muted|success|warning|danger|accent. " + "Compose freely rather than targeting named shapes."; -const TONE_ENUM = ["default", "muted", "success", "warning", "danger", "accent"] as const; +const TONE_ENUM = [ + "default", + "muted", + "success", + "warning", + "danger", + "accent", +] as const; const ALIGN_ENUM = ["left", "right", "center"] as const; const GAP_ENUM = [0, 1] as const; @@ -177,7 +184,9 @@ function rewritePresentForKimi(def: ToolDefinition): ToolDefinition { return { ...def, // structuredClone so callers cannot mutate the shared const via the tool def. - inputSchema: structuredClone(KIMI_PRESENT_INPUT_SCHEMA) as ToolDefinition["inputSchema"], + inputSchema: structuredClone( + KIMI_PRESENT_INPUT_SCHEMA, + ) as ToolDefinition["inputSchema"], }; } @@ -200,5 +209,7 @@ export function normalizeToolDefinitionsForProvider( if (!isKimiLeafProvider(ctx)) { return defs as ToolDefinition[]; } - return defs.map((def) => (def.name === "present" ? rewritePresentForKimi(def) : def)); + return defs.map((def) => + def.name === "present" ? rewritePresentForKimi(def) : def, + ); } diff --git a/src/agent/tool-search.test.ts b/src/agent/tool-search.test.ts index b7db30ece..17e37a1cf 100644 --- a/src/agent/tool-search.test.ts +++ b/src/agent/tool-search.test.ts @@ -77,7 +77,10 @@ describe("createToolIndex", () => { }); test("orchestrator mode advertises split fleet tools and search_agents", () => { - const advertised = advertisedToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY); + const advertised = advertisedToolNamesForSessionMode( + "orchestrator", + FULL_AVAILABILITY, + ); expect(advertised).not.toContain("task"); expect(advertised).toContain("spawn_agent"); expect(advertised).toContain("wait_agents"); @@ -85,7 +88,10 @@ describe("createToolIndex", () => { }); test("orchestrator mode advertises the fleet verbs", () => { - const advertised = advertisedToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY); + const advertised = advertisedToolNamesForSessionMode( + "orchestrator", + FULL_AVAILABILITY, + ); for (const name of [ "spawn_agent", "wait_agents", @@ -101,12 +107,16 @@ describe("createToolIndex", () => { }); test("manage_tasks is advertised regardless of availability", () => { - expect(coreToolNamesForSessionMode("orchestrator", NO_AVAILABILITY)).toContain("manage_tasks"); + expect( + coreToolNamesForSessionMode("orchestrator", NO_AVAILABILITY), + ).toContain("manage_tasks"); }); test("present is never in the advertised core set — discovered via tool_search only", () => { expect(CORE_TOOL_NAMES).not.toContain("present"); - expect(coreToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY)).not.toContain("present"); + expect( + coreToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY), + ).not.toContain("present"); }); test("primary CORE includes product mutation tools; CATALOG does not duplicate them", () => { @@ -121,30 +131,44 @@ describe("createToolIndex", () => { test("catalog advertises web_fetch and web_search so URL work needs no tool_search", () => { expect(CATALOG_TOOL_NAMES).toContain("web_fetch"); expect(CATALOG_TOOL_NAMES).toContain("web_search"); - const advertised = advertisedToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY); + const advertised = advertisedToolNamesForSessionMode( + "orchestrator", + FULL_AVAILABILITY, + ); expect(advertised).toContain("web_fetch"); expect(advertised).toContain("web_search"); }); test("skill_search is catalog-advertised at the end, never CORE", () => { expect(CORE_TOOL_NAMES).not.toContain("skill_search"); - expect(CATALOG_TOOL_NAMES[CATALOG_TOOL_NAMES.length - 1]).toBe("skill_search"); - const advertised = advertisedToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY); + expect(CATALOG_TOOL_NAMES[CATALOG_TOOL_NAMES.length - 1]).toBe( + "skill_search", + ); + const advertised = advertisedToolNamesForSessionMode( + "orchestrator", + FULL_AVAILABILITY, + ); expect(advertised).toContain("skill_search"); expect(advertised[advertised.length - 1]).toBe("skill_search"); }); test("lsp is advertised only when a language server was detected at startup", () => { expect( - coreToolNamesForSessionMode("orchestrator", { languageServerAvailable: true }), + coreToolNamesForSessionMode("orchestrator", { + languageServerAvailable: true, + }), ).toContain("lsp"); expect( - coreToolNamesForSessionMode("orchestrator", { languageServerAvailable: false }), + coreToolNamesForSessionMode("orchestrator", { + languageServerAvailable: false, + }), ).not.toContain("lsp"); }); test("ask_operator is advertised regardless of availability", () => { - expect(coreToolNamesForSessionMode("orchestrator", NO_AVAILABILITY)).toContain("ask_operator"); + expect( + coreToolNamesForSessionMode("orchestrator", NO_AVAILABILITY), + ).toContain("ask_operator"); }); test("the advertised set is deterministic — repeat calls with the same inputs are identical", () => { @@ -212,7 +236,9 @@ describe("createToolSearchTool", () => { lookup: () => undefined, promote: () => undefined, }); - expect(await call(tool, { query: "nonsense" })).toContain("No tools matched"); + expect(await call(tool, { query: "nonsense" })).toContain( + "No tools matched", + ); }); }); @@ -241,7 +267,10 @@ describe("advertisedTools", () => { ]; test("orchestrator wire prefix names include multi-agent tools", () => { - const prefix = advertisedToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY); + const prefix = advertisedToolNamesForSessionMode( + "orchestrator", + FULL_AVAILABILITY, + ); expect(prefix).not.toContain("task"); expect(prefix).toContain("search_agents"); for (const name of [ @@ -287,12 +316,14 @@ describe("advertisedTools", () => { test("the fixed built-in prefix order never changes, activated or not", () => { const forward = advertisedTools(registry).map((d) => d.name); - const reversed = advertisedTools([...registry].reverse()).map((d) => d.name); - expect(reversed).toEqual(forward); - - const withActivation = advertisedTools(registry, ["mcp__linear__create_issue"]).map( + const reversed = advertisedTools([...registry].reverse()).map( (d) => d.name, ); + expect(reversed).toEqual(forward); + + const withActivation = advertisedTools(registry, [ + "mcp__linear__create_issue", + ]).map((d) => d.name); expect(withActivation.slice(0, forward.length)).toEqual(forward); }); @@ -307,13 +338,17 @@ describe("advertisedTools", () => { }); test("repeated activation of the same tool does not reorder or duplicate it", () => { - const once = advertisedTools(registry, ["mcp__linear__create_issue"]).map((d) => d.name); + const once = advertisedTools(registry, ["mcp__linear__create_issue"]).map( + (d) => d.name, + ); const twice = advertisedTools(registry, [ "mcp__linear__create_issue", "mcp__linear__create_issue", ]).map((d) => d.name); expect(twice).toEqual(once); - expect(twice.filter((n) => n === "mcp__linear__create_issue")).toHaveLength(1); + expect(twice.filter((n) => n === "mcp__linear__create_issue")).toHaveLength( + 1, + ); }); test("multiple activations append in first-activation order regardless of registry order", () => { @@ -325,11 +360,15 @@ describe("advertisedTools", () => { inputSchema: { type: "object", properties: {}, required: [] }, }, ]; - const names = advertisedTools(multi, ["mcp__acme__do", "mcp__linear__create_issue"]).map( - (d) => d.name, - ); + const names = advertisedTools(multi, [ + "mcp__acme__do", + "mcp__linear__create_issue", + ]).map((d) => d.name); const tailIdx = names.length - 2; - expect(names.slice(tailIdx)).toEqual(["mcp__acme__do", "mcp__linear__create_issue"]); + expect(names.slice(tailIdx)).toEqual([ + "mcp__acme__do", + "mcp__linear__create_issue", + ]); }); test("the built-in prefix is byte-identical across repeated turns of the same session", () => { @@ -341,7 +380,9 @@ describe("advertisedTools", () => { }); const turn1 = JSON.stringify(advertisedTools(registry, [], prefix)); const turn2 = JSON.stringify(advertisedTools(registry, [], prefix)); - const turn3 = JSON.stringify(advertisedTools(registry, ["mcp__linear__create_issue"], prefix)); + const turn3 = JSON.stringify( + advertisedTools(registry, ["mcp__linear__create_issue"], prefix), + ); expect(turn2).toBe(turn1); // Growth from a mid-session discovery only appends — the prefix itself // (everything before the activated tail) still matches turn 1 exactly. @@ -380,7 +421,10 @@ describe("createActivatedToolTracker", () => { const tracker = createActivatedToolTracker(); tracker.activate(["mcp__acme__do", "mcp__linear__create_issue"]); expect(tracker.activate(["mcp__linear__create_issue"])).toBe(false); - expect(tracker.list()).toEqual(["mcp__acme__do", "mcp__linear__create_issue"]); + expect(tracker.list()).toEqual([ + "mcp__acme__do", + "mcp__linear__create_issue", + ]); }); test("preserves first-activation order across separate calls", () => { diff --git a/src/agent/tool-search.ts b/src/agent/tool-search.ts index 9d42e0a22..13ca2f042 100644 --- a/src/agent/tool-search.ts +++ b/src/agent/tool-search.ts @@ -79,7 +79,8 @@ export function coreToolNamesForSessionMode( ): readonly string[] { const orchestratorEnabled = sessionModeEnablesSubAgents(mode); return CORE_TOOL_NAMES.filter((name) => { - if (!orchestratorEnabled && ORCHESTRATOR_ONLY_TOOL_NAMES.includes(name)) return false; + if (!orchestratorEnabled && ORCHESTRATOR_ONLY_TOOL_NAMES.includes(name)) + return false; if (name === "lsp") return availability.languageServerAvailable; return true; }); @@ -89,7 +90,10 @@ export function advertisedToolNamesForSessionMode( mode: SessionMode, availability: ToolAvailability, ): readonly string[] { - return [...coreToolNamesForSessionMode(mode, availability), ...CATALOG_TOOL_NAMES]; + return [ + ...coreToolNamesForSessionMode(mode, availability), + ...CATALOG_TOOL_NAMES, + ]; } // Built-in file/search/web tools advertised alongside the core set. They carry full @@ -119,7 +123,10 @@ export const CATALOG_TOOL_NAMES: readonly string[] = [ // Primary TUI/exec sessions should pass // `advertisedToolNamesForSessionMode(sessionMode, toolAvailability)` as the // `builtInPrefix` to `advertisedTools` — not this constant alone. -export const ADVERTISED_TOOL_NAMES: readonly string[] = [...CORE_TOOL_NAMES, ...CATALOG_TOOL_NAMES]; +export const ADVERTISED_TOOL_NAMES: readonly string[] = [ + ...CORE_TOOL_NAMES, + ...CATALOG_TOOL_NAMES, +]; // Project the live tool registry onto the advertised set: the fixed built-in // prefix (its order never changes — this is what keeps the provider cache @@ -184,7 +191,10 @@ export const toolSearchDefinition: ToolDefinition = { inputSchema: { type: "object", properties: { - query: { type: "string", description: "A short description of the capability you need." }, + query: { + type: "string", + description: "A short description of the capability you need.", + }, }, required: ["query"], }, @@ -206,7 +216,11 @@ export function createToolIndex( getDefs: () => readonly ToolDefinition[], advertisedNames: readonly string[] = ADVERTISED_TOOL_NAMES, ): ToolIndex { - const score = (def: ToolDefinition, queryTokens: string[], rawQuery: string): number => { + const score = ( + def: ToolDefinition, + queryTokens: string[], + rawQuery: string, + ): number => { const nameTokens = tokenize(def.name); const descTokens = new Set(tokenize(def.description ?? "")); let total = 0; @@ -214,7 +228,8 @@ export function createToolIndex( if (nameTokens.includes(token)) total += 3; else if (descTokens.has(token)) total += 1; else if (def.name.toLowerCase().includes(token)) total += 0.75; - else if ((def.description ?? "").toLowerCase().includes(token)) total += 0.25; + else if ((def.description ?? "").toLowerCase().includes(token)) + total += 0.25; } if (def.name.toLowerCase().includes(rawQuery)) total += 1; return total; @@ -227,7 +242,10 @@ export function createToolIndex( if (queryTokens.length === 0) return []; return getDefs() .filter((def) => !advertisedNames.includes(def.name)) - .map((def) => ({ name: def.name, score: score(def, queryTokens, rawQuery) })) + .map((def) => ({ + name: def.name, + score: score(def, queryTokens, rawQuery), + })) .filter((entry) => entry.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) @@ -274,7 +292,8 @@ export function createToolSearchTool(deps: ToolSearchDeps): AgentTool { return "Error: tool_search requires query (string)."; } const query = parsed.query.trim(); - if (query.length === 0) return "Error: tool_search requires a non-empty query."; + if (query.length === 0) + return "Error: tool_search requires a non-empty query."; const names = deps.search(query); if (names.length === 0) { return `No tools matched "${query}". Try different keywords describing the capability.`; @@ -286,7 +305,9 @@ export function createToolSearchTool(deps: ToolSearchDeps): AgentTool { // so the model can shape arguments this same turn, before the promoted // definition round-trips through the next infer call. deps.promote(names); - const blocks = names.map((name) => renderToolCard(deps.lookup(name), name)); + const blocks = names.map((name) => + renderToolCard(deps.lookup(name), name), + ); return `These tools are available — you can call them now:\n\n${blocks.join("\n\n")}`; }, }); diff --git a/src/agent/tools-mcp-disconnect.test.ts b/src/agent/tools-mcp-disconnect.test.ts index e583515aa..4a167b7f1 100644 --- a/src/agent/tools-mcp-disconnect.test.ts +++ b/src/agent/tools-mcp-disconnect.test.ts @@ -3,7 +3,10 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { withMockedModule } from "../../tests/helpers/mock-module.js"; -import { createExaMCPServerConfig, type ResolvedMCPServerConfig } from "../mcp/exa.js"; +import { + createExaMCPServerConfig, + type ResolvedMCPServerConfig, +} from "../mcp/exa.js"; import type { MCPConnectOptions, MCPTool } from "../mcp/client.js"; import { createPermissionGate } from "../permission/gate.js"; import type { MCPServerState } from "./tools.js"; @@ -17,7 +20,9 @@ let releaseDeferredConnect: (() => void) | undefined; let connectMode: "success" | "deferred" = "success"; // Reconnect tests repoint this to simulate a server whose tool set drifted // between generations; the default matches the original static payload. -let connectedTools: MCPTool[] = [{ name: "list", description: "List", inputSchema: {} }]; +let connectedTools: MCPTool[] = [ + { name: "list", description: "List", inputSchema: {} }, +]; function tempCwd(): string { const dir = mkdtempSync(join(tmpdir(), "corbits-mcp-disconnect-")); @@ -29,7 +34,10 @@ await withMockedModule( import.meta.resolve("../mcp/client.js"), (real: typeof import("../mcp/client.js")) => ({ ...real, - connectMCPServer: async (config: ResolvedMCPServerConfig, options: MCPConnectOptions = {}) => { + connectMCPServer: async ( + config: ResolvedMCPServerConfig, + options: MCPConnectOptions = {}, + ) => { connectOptions.push(options); const generation = ++connectGeneration; if (connectMode === "deferred") { @@ -37,10 +45,15 @@ await withMockedModule( releaseDeferredConnect = resolve; const onAbort = (): void => resolve(); if (options.signal?.aborted === true) onAbort(); - else options.signal?.addEventListener("abort", onAbort, { once: true }); + else + options.signal?.addEventListener("abort", onAbort, { once: true }); }); if (options.signal?.aborted === true) { - return { ok: false as const, serverName: config.name, error: "aborted" }; + return { + ok: false as const, + serverName: config.name, + error: "aborted", + }; } } return { @@ -80,10 +93,26 @@ async function makeToolset() { }); } -const acme = { name: "acme", type: "http" as const, url: "https://mcp.acme.test/mcp" }; -const lin = { name: "lin", type: "http" as const, url: "https://mcp.lin.test/mcp" }; -const linear = { name: "linear", type: "http" as const, url: "https://mcp.linear.test/mcp" }; -const customExa = { name: "exa", type: "http" as const, url: "https://custom.exa.test/mcp" }; +const acme = { + name: "acme", + type: "http" as const, + url: "https://mcp.acme.test/mcp", +}; +const lin = { + name: "lin", + type: "http" as const, + url: "https://mcp.lin.test/mcp", +}; +const linear = { + name: "linear", + type: "http" as const, + url: "https://mcp.linear.test/mcp", +}; +const customExa = { + name: "exa", + type: "http" as const, + url: "https://custom.exa.test/mcp", +}; function callbacks(states: MCPServerState[], toolsChanged: number[] = []) { return { @@ -134,21 +163,29 @@ describe("disconnectMCPServer", () => { cwd: tempCwd(), permissionGate: gate, onOperatorGate: async () => ({ kind: "cancel" }), - mcpServers: resolveMcpServers([{ name: "exa", enabled: false }], undefined), + mcpServers: resolveMcpServers( + [{ name: "exa", enabled: false }], + undefined, + ), }); const states: MCPServerState[] = []; const toolsChanged: number[] = []; try { await toolset.connectMCPServer(acme, callbacks(states, toolsChanged)); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__acme__list", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__acme__list"); expect(toolset.hasMCPServer("acme")).toBe(true); - await toolset.disconnectMCPServer("acme", callbacks(states, toolsChanged)); + await toolset.disconnectMCPServer( + "acme", + callbacks(states, toolsChanged), + ); expect( - toolset.dynamicRunner.currentDefinitions().some((d) => d.name.startsWith("mcp__acme__")), + toolset.dynamicRunner + .currentDefinitions() + .some((d) => d.name.startsWith("mcp__acme__")), ).toBe(false); expect(unregistrations).toBeGreaterThan(0); expect(closedClients).toEqual(["acme"]); @@ -169,9 +206,9 @@ describe("disconnectMCPServer", () => { await toolset.disconnectMCPServer("acme", callbacks(states)); await toolset.connectMCPServer(acme, callbacks(states)); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__acme__list", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__acme__list"); expect(toolset.hasMCPServer("acme")).toBe(true); expect(states.some((s) => s.state === "failed")).toBe(false); } finally { @@ -182,17 +219,29 @@ describe("disconnectMCPServer", () => { test("reconnect after the server's tools drift swaps the mounted set", async () => { const toolset = await makeToolset(); const states: MCPServerState[] = []; - const announced: ReturnType[] = []; + const announced: ReturnType< + typeof toolset.dynamicRunner.currentDefinitions + >[] = []; try { await toolset.connectMCPServer(acme, callbacks(states)); - const acmeNames = (defs: ReturnType) => - defs.map((d) => d.name).filter((name) => name.startsWith("mcp__acme__")); - expect(acmeNames(toolset.dynamicRunner.currentDefinitions())).toEqual(["mcp__acme__list"]); + const acmeNames = ( + defs: ReturnType, + ) => + defs + .map((d) => d.name) + .filter((name) => name.startsWith("mcp__acme__")); + expect(acmeNames(toolset.dynamicRunner.currentDefinitions())).toEqual([ + "mcp__acme__list", + ]); // The server redeployed mid-session: same tool name, new schema, plus a // new tool. Reconnect must mount exactly the drifted set. connectedTools = [ - { name: "list", description: "List v2", inputSchema: { type: "object", required: ["q"] } }, + { + name: "list", + description: "List v2", + inputSchema: { type: "object", required: ["q"] }, + }, { name: "search", description: "Search", inputSchema: {} }, ]; await toolset.disconnectMCPServer("acme", callbacks(states)); @@ -212,7 +261,10 @@ describe("disconnectMCPServer", () => { // The stale generation's client was closed and the drift was announced. expect(closedGenerations).toContain(1); - expect(acmeNames(announced.at(-1) ?? [])).toEqual(["mcp__acme__list", "mcp__acme__search"]); + expect(acmeNames(announced.at(-1) ?? [])).toEqual([ + "mcp__acme__list", + "mcp__acme__search", + ]); } finally { await toolset.dispose(); } @@ -224,13 +276,17 @@ describe("disconnectMCPServer", () => { try { await toolset.connectMCPServer(lin, callbacks(states)); await toolset.connectMCPServer(linear, callbacks(states)); - const names = toolset.dynamicRunner.currentDefinitions().map((d) => d.name); + const names = toolset.dynamicRunner + .currentDefinitions() + .map((d) => d.name); expect(names).toContain("mcp__lin__list"); expect(names).toContain("mcp__linear__list"); await toolset.disconnectMCPServer("lin", callbacks(states)); - const after = toolset.dynamicRunner.currentDefinitions().map((d) => d.name); + const after = toolset.dynamicRunner + .currentDefinitions() + .map((d) => d.name); expect(after).not.toContain("mcp__lin__list"); expect(after).toContain("mcp__linear__list"); expect(toolset.hasMCPServer("linear")).toBe(true); @@ -245,20 +301,25 @@ describe("disconnectMCPServer", () => { const states: MCPServerState[] = []; try { await toolset.connectMCPServer(customExa, callbacks(states)); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__exa__list", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__exa__list"); await toolset.disconnectMCPServer("exa", callbacks(states)); expect( - toolset.dynamicRunner.currentDefinitions().some((d) => d.name.startsWith("mcp__exa__")), + toolset.dynamicRunner + .currentDefinitions() + .some((d) => d.name.startsWith("mcp__exa__")), ).toBe(false); - await toolset.connectMCPServer(createExaMCPServerConfig(), callbacks(states)); - expect(toolset.hasMCPServer("exa")).toBe(true); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__exa__list", + await toolset.connectMCPServer( + createExaMCPServerConfig(), + callbacks(states), ); + expect(toolset.hasMCPServer("exa")).toBe(true); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__exa__list"); } finally { await toolset.dispose(); } @@ -273,14 +334,19 @@ describe("disconnectMCPServer", () => { await waitForConnectStart(); expect(toolset.hasMCPServer("acme")).toBe(true); - const disconnecting = toolset.disconnectMCPServer("acme", callbacks(states)); + const disconnecting = toolset.disconnectMCPServer( + "acme", + callbacks(states), + ); expect(toolset.hasMCPServer("acme")).toBe(true); await Promise.all([connecting, disconnecting]); expect(states.some((s) => s.state === "failed")).toBe(false); expect(states.map((s) => s.state)).toContain("disconnected"); expect( - toolset.dynamicRunner.currentDefinitions().some((d) => d.name.startsWith("mcp__acme__")), + toolset.dynamicRunner + .currentDefinitions() + .some((d) => d.name.startsWith("mcp__acme__")), ).toBe(false); expect(toolset.hasMCPServer("acme")).toBe(false); } finally { @@ -295,7 +361,10 @@ describe("disconnectMCPServer", () => { const toolsChanged: number[] = []; try { expect(toolset.hasMCPServer("ghost")).toBe(false); - await toolset.disconnectMCPServer("ghost", callbacks(states, toolsChanged)); + await toolset.disconnectMCPServer( + "ghost", + callbacks(states, toolsChanged), + ); expect(toolset.hasMCPServer("ghost")).toBe(false); expect(states).toEqual([{ name: "ghost", state: "disconnected" }]); expect(toolsChanged).toHaveLength(1); @@ -311,14 +380,17 @@ describe("disconnectMCPServer", () => { await toolset.connectMCPServer(acme, callbacks(states)); expect(connectOptions).toHaveLength(1); - const disconnecting = toolset.disconnectMCPServer("acme", callbacks(states)); + const disconnecting = toolset.disconnectMCPServer( + "acme", + callbacks(states), + ); const connecting = toolset.connectMCPServer(acme, callbacks(states)); await Promise.all([disconnecting, connecting]); expect(toolset.hasMCPServer("acme")).toBe(true); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__acme__list", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__acme__list"); expect(states.some((s) => s.state === "failed")).toBe(false); expect(closedGenerations).toContain(1); expect(connectOptions).toHaveLength(2); @@ -334,7 +406,10 @@ describe("setMcpServersSource", () => { cwd: tempCwd(), permissionGate: permissionGate(), onOperatorGate: async () => ({ kind: "cancel" }), - mcpServers: resolveMcpServers([{ name: "exa", enabled: false }], undefined), + mcpServers: resolveMcpServers( + [{ name: "exa", enabled: false }], + undefined, + ), mcpServersSource: "local", }); const untrusted: MCPServerState[] = []; @@ -346,7 +421,9 @@ describe("setMcpServersSource", () => { ); expect(toolset.hasMCPServer("acme")).toBe(false); expect( - toolset.dynamicRunner.currentDefinitions().some((d) => d.name.startsWith("mcp__acme__")), + toolset.dynamicRunner + .currentDefinitions() + .some((d) => d.name.startsWith("mcp__acme__")), ).toBe(false); toolset.setMcpServersSource("global"); @@ -354,9 +431,9 @@ describe("setMcpServersSource", () => { await toolset.connectMCPServer(acme, callbacks(trusted)); expect(toolset.hasMCPServer("acme")).toBe(true); - expect(toolset.dynamicRunner.currentDefinitions().map((d) => d.name)).toContain( - "mcp__acme__list", - ); + expect( + toolset.dynamicRunner.currentDefinitions().map((d) => d.name), + ).toContain("mcp__acme__list"); expect(trusted.some((s) => s.state === "connected")).toBe(true); expect(trusted.some((s) => s.state === "failed")).toBe(false); } finally { diff --git a/src/agent/tools.ts b/src/agent/tools.ts index 3e1ccb5bb..73c67bd45 100644 --- a/src/agent/tools.ts +++ b/src/agent/tools.ts @@ -35,7 +35,10 @@ import { import { mcpClientTools } from "../mcp/plugin.js"; import { parseMcpToolName } from "../mcp/tool-name.js"; import { gateAgentTools } from "../plugins/permission-plugin.js"; -import { createDynamicToolRunner, type DynamicToolRunner } from "../tui/dynamic-tool-runner.js"; +import { + createDynamicToolRunner, + type DynamicToolRunner, +} from "../tui/dynamic-tool-runner.js"; import type { MCPServerConfig, Settings } from "../config/settings.js"; import { filterMcpServersForConnect, @@ -45,7 +48,10 @@ import { import type { ToolWatchdogConfig } from "../tui/tool-execution-watchdog.js"; import type { SessionMode } from "../config/session-mode.js"; import { sessionModeEnablesSubAgents } from "../config/session-mode.js"; -import { advertisedToolNamesForSessionMode, type ToolAvailability } from "./tool-search.js"; +import { + advertisedToolNamesForSessionMode, + type ToolAvailability, +} from "./tool-search.js"; import { discoverSkills, type SkillSummary } from "../extensions/skills.js"; import type { ProviderCatalogEntry } from "../config/index.js"; import type { AgentProfile } from "./profiles.js"; @@ -79,8 +85,14 @@ import { type BackgroundShellExit, } from "../shell/background-shell.js"; import { createListDirTool } from "../util/list-dir.js"; -import { createExaMCPWebFetchTool, createWebFetchTool } from "../tools/web-fetch.js"; -import { createWebSearchTool, disposeWebSearchClients } from "../tools/web-search.js"; +import { + createExaMCPWebFetchTool, + createWebFetchTool, +} from "../tools/web-fetch.js"; +import { + createWebSearchTool, + disposeWebSearchClients, +} from "../tools/web-search.js"; import { createUseSkillTool } from "./use-skill.js"; import { createSkillSearchTool } from "./skill-search.js"; import { createToolIndex, createToolSearchTool } from "./tool-search.js"; @@ -121,12 +133,17 @@ const SubmitOutputArgs = type({ // dismiss the question without answering. The gate owns this distinction so the // tool layer can translate each outcome into the right tool result. export type OperatorResult = - { kind: "option"; index: number } | { kind: "custom"; text: string } | { kind: "cancel" }; + | { kind: "option"; index: number } + | { kind: "custom"; text: string } + | { kind: "cancel" }; export interface AgentToolsetArgs { cwd: string; permissionGate: PermissionGate; - onOperatorGate: (question: string, options: string[]) => Promise; + onOperatorGate: ( + question: string, + options: string[], + ) => Promise; mcpServers?: MCPServerConfig[]; /** * Where mcpServers came from. `"local"` requires project trust before spawn; @@ -209,7 +226,9 @@ export interface AgentToolsetArgs { // transcript — child events stay in the store only. sessions?: SubAgentSessionStore; settings?: Settings | (() => Settings | undefined); - catalog?: readonly ProviderCatalogEntry[] | (() => readonly ProviderCatalogEntry[]); + catalog?: + | readonly ProviderCatalogEntry[] + | (() => readonly ProviderCatalogEntry[]); profiles?: AgentProfile[] | (() => AgentProfile[]); // Opt-in: dispatch each sub-agent into its own git worktree instead of // sharing this session's cwd. See src/subagent/worktree.ts. @@ -249,7 +268,10 @@ export interface AgentToolset { dynamicRunner: DynamicToolRunner; // Connect configured MCP servers in the background. Resolves once every server // has either connected or failed; authorization waits are bounded by `signal`. - connectMCP: (callbacks: MCPConnectCallbacks, signal?: AbortSignal) => Promise; + connectMCP: ( + callbacks: MCPConnectCallbacks, + signal?: AbortSignal, + ) => Promise; // Connect one newly persisted server through the same lifecycle as startup MCP. connectMCPServer: ( config: MCPServerConfig, @@ -259,7 +281,10 @@ export interface AgentToolset { // Drop a server's tools in the running session. Idempotent for unknown and // already-disconnected names. Persist uses hasMCPServer as occupied; this // is the live teardown that disable/remove need. - disconnectMCPServer: (name: string, callbacks: MCPConnectCallbacks) => Promise; + disconnectMCPServer: ( + name: string, + callbacks: MCPConnectCallbacks, + ) => Promise; // True while this name is connected or a connection is in flight — not after // teardown, and not after a failed connect. Persist uses this to block a // second add of an active name; failed rows retry through connectMCPServer @@ -282,7 +307,9 @@ export interface AgentToolset { dispose: () => Promise; } -export async function createAgentToolset(args: AgentToolsetArgs): Promise { +export async function createAgentToolset( + args: AgentToolsetArgs, +): Promise { const { cwd, permissionGate, @@ -316,23 +343,39 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise void) | undefined; - let builtinExaConnection: Promise | undefined = builtinExaEnabled - ? new Promise((resolve) => { - resolveBuiltinExaConnection = resolve; - }) - : undefined; + let resolveBuiltinExaConnection: + | ((result: MCPConnectResult) => void) + | undefined; + let builtinExaConnection: Promise | undefined = + builtinExaEnabled + ? new Promise((resolve) => { + resolveBuiltinExaConnection = resolve; + }) + : undefined; - const waitForBuiltinExaConnection = async (signal: AbortSignal): Promise => { + const waitForBuiltinExaConnection = async ( + signal: AbortSignal, + ): Promise => { const pending = builtinExaConnection; if (pending === undefined) { - return { ok: false, serverName: "exa", error: "built-in Exa MCP is not enabled" }; + return { + ok: false, + serverName: "exa", + error: "built-in Exa MCP is not enabled", + }; } if (signal.aborted) { return { @@ -359,12 +402,16 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise { @@ -444,18 +495,23 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise gateAgentTools(inheritedMcpTools, gate), + inheritMcpTools: (gate: PermissionGate) => + gateAgentTools(inheritedMcpTools, gate), ...(shellTimeout !== undefined ? { shellTimeout } : {}), ...(shellEnv !== undefined ? { shellEnv } : {}), ...(extraToolPlugins.length > 0 ? { extraToolPlugins } : {}), cwd, getWorkdirBase: sa.getWorkdirBase, provider: sa.provider, - ...(args.getBlobReader !== undefined ? { getBlobReader: args.getBlobReader } : {}), + ...(args.getBlobReader !== undefined + ? { getBlobReader: args.getBlobReader } + : {}), run: runSubAgent, sessions: fleetSessions, fleetRecords, - ...(sa.useWorktree !== undefined ? { useWorktree: sa.useWorktree } : {}), + ...(sa.useWorktree !== undefined + ? { useWorktree: sa.useWorktree } + : {}), ...(sa.onEvent !== undefined ? { onEvent: sa.onEvent } : {}), ...(sa.onProgress !== undefined ? { onProgress: sa.onProgress } : {}), ...(sa.settings !== undefined ? { settings: sa.settings } : {}), @@ -505,7 +561,10 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise, _signal: AbortSignal): Promise => { + handler: async ( + rawArgs: Record, + _signal: AbortSignal, + ): Promise => { const parsed = AskOperatorArgs(rawArgs); if (parsed instanceof type.errors) { return "Error: ask_operator requires question (string) and options (array of strings)."; @@ -542,7 +601,11 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise= options.length) { return `Error: invalid selection ${index}. Valid range: 0-${options.length - 1}.`; } - return options[index]!; + const selected = options[index]; + if (selected === undefined) { + return `Error: invalid selection ${index}. Valid range: 0-${options.length - 1}.`; + } + return selected; }, }), stringTool({ @@ -565,7 +628,8 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise): Promise => { const parsed = SubmitOutputArgs(rawArgs); const step = parsed instanceof type.errors ? undefined : parsed.step; - const summary = parsed instanceof type.errors ? undefined : parsed.summary; + const summary = + parsed instanceof type.errors ? undefined : parsed.summary; const workflowActive = args.isWorkflowActive?.() === true; if (workflowActive) { if (step === undefined || step.length === 0) { @@ -573,7 +637,10 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise 0 ? ` (${summary})` : ""; + const note = + summary !== undefined && summary.length > 0 + ? ` (${summary})` + : ""; return `Workflow step marked complete${note}. Advancing to the next step.`; } if (result === "already-complete") { @@ -591,7 +658,9 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise void } = { promote: () => undefined }; + const promoter: { promote: (names: string[]) => void } = { + promote: () => undefined, + }; const runnerHolder: { current?: DynamicToolRunner } = {}; const toolIndex = createToolIndex( () => runnerHolder.current?.currentDefinitions() ?? [], @@ -600,7 +669,8 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise toolIndex.search(query), - lookup: (name) => runnerHolder.current?.currentDefinitions().find((d) => d.name === name), + lookup: (name) => + runnerHolder.current?.currentDefinitions().find((d) => d.name === name), promote: (names) => promoter.promote(names), }), ); @@ -616,7 +686,9 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise tool.definition.name !== "apply_patch"); + const primaryTools = baseTools.filter( + (tool) => tool.definition.name !== "apply_patch", + ); const dynamicRunner = createDynamicToolRunner(primaryTools, toolWatchdog); runnerHolder.current = dynamicRunner; @@ -644,7 +716,9 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise Promise): Promise => { + const enqueueServerOp = ( + name: string, + op: () => Promise, + ): Promise => { const previous = serverOpQueues.get(name) ?? Promise.resolve(); const run = previous.then(op, op); const tracked = run.then( @@ -687,12 +767,18 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise { - resolveBuiltinExaConnection?.({ ok: false, serverName: EXA_MCP_SERVER_NAME, error }); + resolveBuiltinExaConnection?.({ + ok: false, + serverName: EXA_MCP_SERVER_NAME, + error, + }); resolveBuiltinExaConnection = undefined; }; const replaceInheritedTool = (toolName: string, tool: AgentTool): void => { - const index = inheritedMcpTools.findIndex((entry) => entry.definition.name === toolName); + const index = inheritedMcpTools.findIndex( + (entry) => entry.definition.name === toolName, + ); if (index >= 0) inheritedMcpTools.splice(index, 1); inheritedMcpTools.push(tool); }; @@ -713,7 +799,9 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise((resolve) => { resolveBuiltinExaConnection = resolve; }); - mountWebFetch(createExaMCPWebFetchTool({ connect: waitForBuiltinExaConnection })); + mountWebFetch( + createExaMCPWebFetchTool({ connect: waitForBuiltinExaConnection }), + ); }; const dropServerTools = (name: string): void => { @@ -724,7 +812,10 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise= 0; i--) { const entry = inheritedMcpTools[i]; - if (entry !== undefined && parseMcpToolName(entry.definition.name)?.server === name) { + if ( + entry !== undefined && + parseMcpToolName(entry.definition.name)?.server === name + ) { inheritedMcpTools.splice(i, 1); } } @@ -748,7 +839,10 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise - disabledNames.has(config.name) || currentEpoch(config.name) !== ownedEpoch; + disabledNames.has(config.name) || + currentEpoch(config.name) !== ownedEpoch; const run = (async () => { if (mcpServersSource === "local") { @@ -812,9 +911,14 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise connectOneMCPServer(config, callbacks, signal))); + await Promise.all( + toConnect.map((config) => connectOneMCPServer(config, callbacks, signal)), + ); if (disposed) return; // Report untrusted local servers as failed (fail closed) so the UI is honest. if (mcpServersSource === "local") { @@ -1018,7 +1140,9 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise client.close().catch(() => undefined)), + [...connectedClients.values()].map((client) => + client.close().catch(() => undefined), + ), ); connectedClients.clear(); await disposeWebSearchClients(); @@ -1032,7 +1156,8 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise connectedClients.has(name) || inFlightConnections.has(name), + hasMCPServer: (name) => + connectedClients.has(name) || inFlightConnections.has(name), setMcpServersSource: (source) => { mcpServersSource = source; }, diff --git a/src/agent/use-skill.ts b/src/agent/use-skill.ts index 076abf3f6..15313b6a7 100644 --- a/src/agent/use-skill.ts +++ b/src/agent/use-skill.ts @@ -17,7 +17,10 @@ const useSkillDefinition: ToolDefinition = { inputSchema: { type: "object", properties: { - name: { type: "string", description: "The skill name to load, as listed under Skills" }, + name: { + type: "string", + description: "The skill name to load, as listed under Skills", + }, }, required: ["name"], }, @@ -31,14 +34,17 @@ export function createUseSkillTool( telemetry: Telemetry = NOOP_TELEMETRY, allowedNames?: readonly string[], ): AgentTool { - const allowed = allowedNames === undefined ? undefined : new Set(allowedNames); + const allowed = + allowedNames === undefined ? undefined : new Set(allowedNames); return stringTool({ definition: useSkillDefinition, handler: async (rawArgs: Record): Promise => { const parsed = UseSkillArgs(rawArgs); - if (parsed instanceof type.errors) return "Error: use_skill requires name (string)."; + if (parsed instanceof type.errors) + return "Error: use_skill requires name (string)."; const name = parsed.name.trim(); - if (name.length === 0) return "Error: use_skill requires a non-empty name."; + if (name.length === 0) + return "Error: use_skill requires a non-empty name."; if (allowed !== undefined && !allowed.has(name)) { return `No skill named "${name}" is available.`; } diff --git a/src/auth/callback-page.test.ts b/src/auth/callback-page.test.ts index 360d6b09d..bd479266c 100644 --- a/src/auth/callback-page.test.ts +++ b/src/auth/callback-page.test.ts @@ -37,7 +37,10 @@ describe("callbackPageHtml", () => { }); test("failure names the server and the humanized reason", () => { - const html = callbackPageHtml({ subject: "granola", error: "access_denied" }); + const html = callbackPageHtml({ + subject: "granola", + error: "access_denied", + }); expect(html).toContain("Granola failed to connect"); expect(html).toContain("Access denied."); expect(html).not.toContain("access_denied"); @@ -45,11 +48,15 @@ describe("callbackPageHtml", () => { test("an unnamed authorization still renders both outcomes", () => { expect(callbackPageHtml()).toContain("Authorization complete"); - expect(callbackPageHtml({ error: "server_error" })).toContain("Authorization did not complete"); + expect(callbackPageHtml({ error: "server_error" })).toContain( + "Authorization did not complete", + ); }); test("the subject is escaped rather than pasted into markup", () => { - expect(callbackPageHtml({ subject: "" })).not.toContain("" })).not.toContain( + "