From 0bbcee7758120a05547acbbbd9481ce7cb4efa64 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 22:39:15 -0700 Subject: [PATCH 01/10] Add a defined helper for required test values --- tests/helpers/defined.test.ts | 14 ++++++++++++++ tests/helpers/defined.ts | 6 ++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/helpers/defined.test.ts create mode 100644 tests/helpers/defined.ts diff --git a/tests/helpers/defined.test.ts b/tests/helpers/defined.test.ts new file mode 100644 index 000000000..61dfe6400 --- /dev/null +++ b/tests/helpers/defined.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from "bun:test"; + +import { defined } from "./defined.js"; + +test("returns a present value", () => { + expect(defined("ok", "label")).toBe("ok"); + expect(defined(0, "zero")).toBe(0); + expect(defined(false, "flag")).toBe(false); +}); + +test("throws when the value is null or undefined", () => { + expect(() => defined(undefined, "missing")).toThrow("expected missing to be defined"); + expect(() => defined(null, "empty")).toThrow("expected empty to be defined"); +}); diff --git a/tests/helpers/defined.ts b/tests/helpers/defined.ts new file mode 100644 index 000000000..7d23ac30e --- /dev/null +++ b/tests/helpers/defined.ts @@ -0,0 +1,6 @@ +export function defined(value: T | null | undefined, label = "value"): T { + if (value == null) { + throw new Error(`expected ${label} to be defined`); + } + return value; +} From 5b7faffaa07dd92e46a5c45f5c79bd710de28b22 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 22:39:21 -0700 Subject: [PATCH 02/10] Add oxlint and oxfmt configs with a mock.module plugin --- .oxfmtrc.json | 28 +++++ .oxlintrc.json | 106 ++++++++++++++++++ bun.lock | 84 ++++++++++++++ package.json | 2 + scripts/oxlint-plugin-corbits.js | 40 +++++++ tests/unit/oxlint-no-bare-mock-module.test.ts | 66 +++++++++++ 6 files changed, 326 insertions(+) create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json create mode 100644 scripts/oxlint-plugin-corbits.js create mode 100644 tests/unit/oxlint-no-bare-mock-module.test.ts 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..2ceba0abb --- /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": "off", + "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": "off" + }, + "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/bun.lock b/bun.lock index 3434dbde7..6aa782b7e 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,8 @@ "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", "eslint": "^9.39.0", + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", "prettier": "^3.6.2", "typescript": "5.9.3", "typescript-eslint": "^8.46.4", @@ -323,6 +325,82 @@ "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ulfw8EHN1MBq/MFFDXw2/M1VAFu5mRUcnuZ8Hqbv9viAnFzO9t1jKSAsDqKYYDGMlytF/uj6Z5z5n/tHupnKhw=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MfONZx/O2o9M5v2jDFol556G9+A+P9xCuJ4DZ+qhE+RnaCdoscy6Eu5nq1dbuNxhwdJyZ6kLI7fnG9mwEeOeGg=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CYnIx5LvFVJnyJcCqwH2jxMKjFjqo5678MPjdmNFoSGMhlOvZ/xRZqvhDcolKrXc8fezW3AKh+C4wyoFuWOSSg=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.67.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7/iF1orvIS9mxhKUqnmtMgm+OrSQ5acPwuvdQrm6ECgqbwPmC+Pw9cdke3sNfVN6pT2hbJ58+jP8BCThl5HXOg=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-yy+OGys07IZOpOmYPZoObKyUQLkfxeQqeCypk+1jaZd8HGo77hzvU1Jg8X3+W75o+9lszOjBfg0nkGtlwYywXw=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wPIeeigXgJpwNw3wydYRt3U9iN9Y/ejpOZuYL9IA7igxWs7LIQMOkhKxTumRvy6dIv0iXKk3RTw3Vmjg0i+2sg=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0+XNxcdbkTfxdcD4qW6Ci9n+mBNJ8xTBumnxKvKBmRFOdx0Wf8/KiHjCJayooXmYkqRpRVd98Q5egvzx5BLSgQ=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-I75LKPJyNOYUzkqAiAMIE31+Ye7xtQXZdoty1IXn4B+bw5Zpmez5wfG19ejGpNnS/BzQ7LFS+7jxuTPb+vHiZw=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.67.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-c2M5iRpe1QMZSRE/UvZoPdXBWb5Ic/ycvOyNiKCqPwQ/OyOKIMiJs02ynlNnjb7ZZJnRXYLmGcohoINOcwDK3w=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-dQzzYlV24Udhfm5ECuSdgqRvFJU/CGHzcYYEO3dLM6W6+CHiBFrq9OjIllkdCcPhsoSQ8o223Dja84MOSzed9A=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-rFNq1CgX4qMJANOq42LkAs90JE80GpiaEohAV2qn/gT2hGjQTW1zBO5zQBxArI4926pM1OSzo3CN0tBszGBIaA=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.67.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Sky6rEdz2o5IGq01lPhS12yEvDdChVEcaYrcLHkveh4Fx0qPjljE/Iul6SX/bRMl6lNc8J7J/mDQdzgBdA++Pg=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vPXmlNORV8AZq2Ocxh07pxwMjfENUWCV/eZArnao0qC3NO/hDeTVkQvee7SJJUbIiF5PZbBa4kYmaXnu7Rk58w=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-x/WAtFqYtVr3vZ9ni8nr4kn9whSitg8fOljq/pZzBpxopRdY1BMLZCZkrbIbaBcYkm46qGbqVea2FCWmtQ2P9w=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.67.0", "", { "os": "none", "cpu": "arm64" }, "sha512-eRw9Neh4/aA6i+q/R3WU1gGQINhVM0J4fXIm6t27caOamkr/37uAkp1IdBx4zlJH97hmXR63z/q9n5c5dN7MzA=="], + + "@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=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.82.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PLEaSD8IAIIlwW4dwOd9YaxuxeOpwiXL4J24rcnE4iNtyM5j9Q9/3+gti08oXpx0u2ygNjRDx9xjWWpQonuJEw=="], + + "@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/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -691,6 +769,10 @@ "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=="], + + "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-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=="], @@ -803,6 +885,8 @@ "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=="], diff --git a/package.json b/package.json index d3aa34638..9616048da 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,8 @@ "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", "eslint": "^9.39.0", + "oxfmt": "^0.67.0", + "oxlint": "^1.82.0", "prettier": "^3.6.2", "typescript": "5.9.3", "typescript-eslint": "^8.46.4", diff --git a/scripts/oxlint-plugin-corbits.js b/scripts/oxlint-plugin-corbits.js new file mode 100644 index 000000000..4339367a3 --- /dev/null +++ b/scripts/oxlint-plugin-corbits.js @@ -0,0 +1,40 @@ +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/tests/unit/oxlint-no-bare-mock-module.test.ts b/tests/unit/oxlint-no-bare-mock-module.test.ts new file mode 100644 index 000000000..902ffeb70 --- /dev/null +++ b/tests/unit/oxlint-no-bare-mock-module.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "bun:test"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +const repoRoot = join(import.meta.dirname, "../.."); +const oxlintrc = join(repoRoot, ".oxlintrc.json"); +const ruleCode = "corbits(no-bare-mock-module)"; + +interface OxlintJson { + diagnostics?: { code?: string; message?: string }[]; +} + +async function runOxlint( + file: string, +): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const proc = Bun.spawn(["bunx", "oxlint", "-c", oxlintrc, "-f", "json", file], { + cwd: repoRoot, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + return { exitCode, stdout, stderr }; +} + +function findingsForRule(stdout: string): unknown[] { + const parsed = JSON.parse(stdout) as OxlintJson; + return (parsed.diagnostics ?? []).filter((item) => item.code === ruleCode); +} + +test("oxlint reports bare mock.module in a *.test.ts file", async () => { + const dir = await mkdtemp(join(import.meta.dirname, "oxlint-mock-module-banned-")); + const file = join(dir, "banned.test.ts"); + try { + await writeFile( + file, + `import { mock } from "bun:test"; +mock.module("./example.js", () => ({})); +`, + ); + const { stdout, stderr } = await runOxlint(file); + expect(findingsForRule(stdout).length, stderr || stdout).toBeGreaterThan(0); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("oxlint is clean when a *.test.ts file only uses withMockedModule", async () => { + const dir = await mkdtemp(join(import.meta.dirname, "oxlint-mock-module-clean-")); + const file = join(dir, "clean.test.ts"); + try { + await writeFile( + file, + `import { withMockedModule } from "../helpers/mock-module.ts"; +await withMockedModule("./example.js", () => ({})); +`, + ); + const { stdout, stderr } = await runOxlint(file); + expect(findingsForRule(stdout), stderr || stdout).toEqual([]); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); From 74ca0c05a69eeefa2b0852b6ff0f7dd4871c3c3d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 22:39:25 -0700 Subject: [PATCH 03/10] Replace TUI empty functions and non-null assertions --- src/tui/agent-ask-wake.test.ts | 24 +-- src/tui/approval-prompt-visibility.test.ts | 3 +- src/tui/command-registry-setup.test.ts | 2 +- src/tui/command-surfaces.test.ts | 148 ++++++------- src/tui/commands/built-in.test.ts | 61 +++--- src/tui/commands/registry.test.ts | 2 +- src/tui/copy-path.test.ts | 18 +- src/tui/copy-path.ts | 2 +- src/tui/copy-wire.test.ts | 4 +- src/tui/decision-truncation.test.ts | 2 +- src/tui/diff-rows.test.ts | 5 +- src/tui/diff.test.ts | 58 +++--- src/tui/diff.ts | 58 ++++-- src/tui/focus/focus-state.test.ts | 5 +- src/tui/focus/focus-state.ts | 3 +- src/tui/gate-wire.test.ts | 28 +-- src/tui/gate-wire.ts | 4 +- src/tui/geometry.test.ts | 57 +++-- src/tui/harness.test.ts | 24 +-- src/tui/image-attachments.test.ts | 3 +- src/tui/keybindings.test.ts | 26 +-- src/tui/landing.test.ts | 29 ++- src/tui/list-modal.test.ts | 3 +- src/tui/list-modal.ts | 2 +- src/tui/live-session-port.test.ts | 8 +- src/tui/markdown-parser.test.ts | 3 +- src/tui/markdown-parser.ts | 51 +++-- src/tui/markdown-rows.test.ts | 83 ++++---- src/tui/mcp-copy-failure.test.ts | 8 +- src/tui/mcp-view.ts | 4 +- src/tui/mention-popup.test.ts | 5 +- src/tui/onboarding.test.ts | 16 +- src/tui/overlay-overflow.test.ts | 35 ++-- src/tui/overlays.test.ts | 25 +-- src/tui/palette-paint.test.ts | 28 +-- src/tui/plugins-admin-backend.ts | 2 +- src/tui/product-host.test.ts | 40 ++-- src/tui/prompt-border.test.ts | 39 ++-- src/tui/prompt-box.test.ts | 6 +- src/tui/prompt-chrome.test.ts | 10 +- src/tui/prompt-features.test.ts | 24 +-- src/tui/prompt-kill-ring.test.ts | 19 +- src/tui/prompt-kill-ring.ts | 15 +- src/tui/prompt-slash-exit.test.ts | 4 +- src/tui/provider-connect.test.ts | 3 +- src/tui/provider-setup-submit.test.ts | 4 +- src/tui/provider-setup.test.ts | 68 +++--- src/tui/provider/setup.ts | 2 +- src/tui/queued-delivery-hop.test.ts | 6 +- src/tui/queued-delivery.test.ts | 8 +- src/tui/row-click.test.ts | 7 +- src/tui/row-retext.test.ts | 5 +- src/tui/row-update-perf.test.ts | 2 +- src/tui/runner-host.test.ts | 232 ++++++++++----------- src/tui/runner/exit.test.ts | 9 +- src/tui/runner/session.ts | 5 +- src/tui/runner/wiring.ask-wake.test.ts | 20 +- src/tui/runtime-bridge-coalesce.test.ts | 4 +- src/tui/runtime-bridge.test.ts | 25 +-- src/tui/runtime-bridge.ts | 8 +- src/tui/runtime-channels.test.ts | 11 +- src/tui/runtime-shutdown.test.ts | 11 +- src/tui/selection-copy.test.ts | 5 +- src/tui/sent-message-history.test.ts | 21 +- src/tui/sent-message-history.ts | 9 +- src/tui/session-chrome.test.ts | 9 +- src/tui/session-operation-queue.test.ts | 2 +- src/tui/session-queue.test.ts | 9 +- src/tui/session-start.ts | 4 +- src/tui/shell.test.ts | 17 +- src/tui/shell/chrome.ts | 5 +- src/tui/shell/transcript.ts | 6 +- src/tui/slash-popup-gate.test.ts | 2 +- src/tui/stream.test.ts | 5 +- src/tui/submit-handler.test.ts | 2 +- src/tui/syntax-highlight.ts | 4 +- src/tui/thinking-reveal.test.ts | 3 +- src/tui/tool-execution-watchdog.test.ts | 21 +- src/tui/tool-execution-watchdog.ts | 6 +- src/tui/tool-formatter.ts | 24 ++- src/tui/tool-rows.test.ts | 11 +- src/tui/turn-monitor.test.ts | 2 +- src/tui/turns-to-blocks.ts | 8 +- src/tui/view/height.ts | 14 +- src/tui/view/lines.ts | 21 +- src/tui/wave6.test.ts | 17 +- src/tui/welcome.ts | 2 +- src/tui/workspace-watch.test.ts | 2 +- 88 files changed, 895 insertions(+), 762 deletions(-) diff --git a/src/tui/agent-ask-wake.test.ts b/src/tui/agent-ask-wake.test.ts index de7b5ea81..1d0fa29f0 100644 --- a/src/tui/agent-ask-wake.test.ts +++ b/src/tui/agent-ask-wake.test.ts @@ -45,7 +45,7 @@ async function withWakeBridge( }; const bridge = attachSessionBridge( shell, - createLiveSessionPort({ send, deliver: send, interrupt: () => {} }), + createLiveSessionPort({ send, deliver: send, interrupt: () => undefined }), ); try { run(bridge, sends); @@ -72,9 +72,9 @@ for (const action of ["retry", "interrupt", "reset", "dispose", "composer", "ord const feedback: string[] = []; let cancellations = 0; let nowMs = 0; - let tick = () => {}; + let tick: () => void = () => undefined; const submit = createSubmitHandler({ - dispatchCommand: () => {}, + dispatchCommand: () => undefined, sendPrompt: (text) => { composerSends.push(text); sends.push(text); @@ -97,7 +97,7 @@ for (const action of ["retry", "interrupt", "reset", "dispose", "composer", "ord feedbackPending: isFeedbackCapturePending(), feedbackCaptureEnabled: true, }), - interrupt: () => {}, + interrupt: () => undefined, deliver: routeQueuedDelivery({ send: (text) => { sends.push(text); @@ -113,7 +113,7 @@ for (const action of ["retry", "interrupt", "reset", "dispose", "composer", "ord schedule: (fn) => { tick = fn; // Retain the callback to exercise even a stale timer after disposal. - return () => {}; + return () => undefined; }, }); try { @@ -190,7 +190,7 @@ describe("agent ask wake delivery", () => { const feedback: string[] = []; let cancellations = 0; const submit = createSubmitHandler({ - dispatchCommand: () => {}, + dispatchCommand: () => undefined, sendPrompt: (text) => { sends.push(text); }, @@ -212,7 +212,7 @@ describe("agent ask wake delivery", () => { feedbackPending: isFeedbackCapturePending(), feedbackCaptureEnabled: true, }), - interrupt: () => {}, + interrupt: () => undefined, deliver: routeQueuedDelivery({ send: (text) => { sends.push(text); @@ -396,7 +396,7 @@ describe("agent ask wake delivery", () => { }); const sends: string[] = []; let nowMs = 0; - let tick = () => {}; + let tick: () => void = () => undefined; const bridge = attachSessionBridge( shell, createLiveSessionPort({ @@ -406,7 +406,7 @@ describe("agent ask wake delivery", () => { deliver: (text) => { sends.push(text); }, - interrupt: () => {}, + interrupt: () => undefined, }), { now: () => nowMs, @@ -414,7 +414,7 @@ describe("agent ask wake delivery", () => { stallNoticeMs: 400, schedule: (fn) => { tick = fn; - return () => {}; + return () => undefined; }, }, ); @@ -456,7 +456,7 @@ describe("agent ask wake delivery", () => { }; const bridge = attachSessionBridge( shell, - createLiveSessionPort({ send, deliver: send, interrupt: () => {} }), + createLiveSessionPort({ send, deliver: send, interrupt: () => undefined }), ); try { const ask = { @@ -537,7 +537,7 @@ describe("agent ask wake delivery", () => { }, parentCycleLive: () => bridge.parentCycleLive, }), - interrupt: () => {}, + interrupt: () => undefined, }), ); try { diff --git a/src/tui/approval-prompt-visibility.test.ts b/src/tui/approval-prompt-visibility.test.ts index 66f23a07f..81a6f8a37 100644 --- a/src/tui/approval-prompt-visibility.test.ts +++ b/src/tui/approval-prompt-visibility.test.ts @@ -5,6 +5,7 @@ * the prompt box's growth and over the overlay's own context text. */ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { makePermissionItems, withTestRenderer } from "./harness.js"; import { appendStreamRow } from "./shell/chrome.js"; import { createAppShell } from "./shell/index.js"; @@ -122,7 +123,7 @@ describe("approval overlay keeps the prompt box on screen (CL-5750)", () => { expect(lines.some((l) => l.includes("╭"))).toBe(true); expect(lines.some((l) => l.includes("╰"))).toBe(true); expect(shell.overlayList).not.toBeNull(); - expect(shell.overlayList!.height).toBeGreaterThanOrEqual(1); + expect(defined(shell.overlayList).height).toBeGreaterThanOrEqual(1); } finally { shell.dispose(); } diff --git a/src/tui/command-registry-setup.test.ts b/src/tui/command-registry-setup.test.ts index e3093e51d..09db63256 100644 --- a/src/tui/command-registry-setup.test.ts +++ b/src/tui/command-registry-setup.test.ts @@ -59,7 +59,7 @@ describe("session command registry setup", () => { ); expect(getCommand("live-config-command")?.description).toBe("enabled"); - expect(getCommand("live-config-command")?.handler("", { signalClear: () => {} })).toEqual({ + expect(getCommand("live-config-command")?.handler("", { signalClear: () => undefined })).toEqual({ type: "message", text: "enabled", }); diff --git a/src/tui/command-surfaces.test.ts b/src/tui/command-surfaces.test.ts index 9666e6e8c..e508d913d 100644 --- a/src/tui/command-surfaces.test.ts +++ b/src/tui/command-surfaces.test.ts @@ -5,6 +5,8 @@ import { describe, expect, test } from "bun:test"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; +import { defined } from "../../tests/helpers/defined.js"; + import { grantRowLabel, openCommandSurface, @@ -151,7 +153,7 @@ function settingsDeps(overrides?: Partial): { showPromptCost: [] as boolean[], }; const deps: CommandSurfaceDeps = { - notify: () => {}, + notify: () => undefined, settings: { read: () => state, setCompactionMode: (mode) => { @@ -259,7 +261,7 @@ describe("settings surface", () => { test("arrow navigation still moves the cursor in a non-cycling overlay", async () => { await withShell(async (shell) => { const deps: CommandSurfaceDeps = { - notify: () => {}, + notify: () => undefined, // Only the fields this test exercises; the rest of PluginsSurfaceDeps // (credentials, verify, web providers) belongs to the plugins surface, // not to this arrow-navigation scoping test. @@ -291,7 +293,7 @@ describe("permissions surface", () => { ]; const revoked: string[] = []; const deps: CommandSurfaceDeps = { - notify: () => {}, + notify: () => undefined, permissions: { list: () => Promise.resolve(grants), revoke: (id) => { @@ -316,7 +318,7 @@ describe("permissions surface", () => { test("empty grant list still opens with a hint row", async () => { await withShell(async (shell) => { const deps: CommandSurfaceDeps = { - notify: () => {}, + notify: () => undefined, permissions: { list: () => Promise.resolve([]), revoke: () => Promise.resolve() }, }; openCommandSurface(shell, "permissions", deps); @@ -343,7 +345,7 @@ describe("plugins surface", () => { ["exa", true], ]); const deps: CommandSurfaceDeps = { - notify: () => {}, + notify: () => undefined, // Same partial-mock rationale as the arrow-navigation test above. plugins: { list: () => @@ -481,7 +483,7 @@ describe("plugins surface admin actions", () => { agentProfiles: [{ id: "a" }], }); // pluginActionDeps builds PluginsSurfaceDeps without loadWarnings; splice it in. - const plugins = deps.plugins!; + const plugins = defined(deps.plugins, "plugins"); const withWarnings: CommandSurfaceDeps = { ...deps, plugins: { @@ -691,7 +693,7 @@ describe("plugins surface admin actions", () => { test("empty plugin list Alt+A still opens add-path", async () => { await withShell(async (shell) => { const { deps, calls } = pluginActionDeps(); - const plugins = deps.plugins!; + const plugins = defined(deps.plugins, "plugins"); const empty: CommandSurfaceDeps = { ...deps, plugins: { ...plugins, list: () => [] }, @@ -708,7 +710,7 @@ describe("plugins surface admin actions", () => { test("Alt+A on warnings/Close still opens add-path", async () => { await withShell(async (shell) => { const { deps, calls } = pluginActionDeps(); - const plugins = deps.plugins!; + const plugins = defined(deps.plugins, "plugins"); const withWarnings: CommandSurfaceDeps = { ...deps, plugins: { @@ -730,7 +732,7 @@ describe("plugins surface admin actions", () => { test("empty plugin list Alt+W still opens web chooser", async () => { await withShell(async (shell) => { const { deps, calls } = pluginActionDeps(); - const plugins = deps.plugins!; + const plugins = defined(deps.plugins, "plugins"); const empty: CommandSurfaceDeps = { ...deps, plugins: { ...plugins, list: () => [] }, @@ -748,7 +750,7 @@ describe("plugins surface admin actions", () => { test("Alt+X on warnings/Close is a no-op", async () => { await withShell(async (shell) => { const { deps, calls } = pluginActionDeps(); - const plugins = deps.plugins!; + const plugins = defined(deps.plugins, "plugins"); const withWarnings: CommandSurfaceDeps = { ...deps, plugins: { @@ -800,7 +802,7 @@ describe("plugins surface admin actions", () => { test("description zone names disable-only for bundled and Claude plugins", () => { const { deps } = pluginActionDeps(); - const plugins = deps.plugins!; + const plugins = defined(deps.plugins, "plugins"); const bundled = pluginDescription( { id: "corbits-skills", @@ -849,7 +851,7 @@ describe("hooks surface", () => { await withShell(async (shell) => { const state = new Map([["/hooks/a.ts", true]]); const deps: CommandSurfaceDeps = { - notify: () => {}, + notify: () => undefined, hooks: { list: () => [...state].map(([id, enabled]) => ({ @@ -888,8 +890,8 @@ describe("mcp surface", () => { test("lists every configured server with its live state", async () => { await withShell((shell) => { openCommandSurface(shell, "mcp", { - notify: () => {}, - mcp: { list: () => entries, openAuthURL: () => {} }, + notify: () => undefined, + mcp: { list: () => entries, openAuthURL: () => undefined }, }); expect(shell.overlayItems.slice(0, 3)).toEqual([ "linear — connected · 12 tools", @@ -903,10 +905,10 @@ describe("mcp surface", () => { test("hides the add row while local MCP settings shadow global", async () => { await withShell((shell) => { openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, mcpServersSource: "local", addServer: async () => ({ ok: true, message: "should not run" }), }, @@ -920,8 +922,8 @@ describe("mcp surface", () => { test("empty MCP list uses a placeholder distinct from close", async () => { await withShell((shell) => { openCommandSurface(shell, "mcp", { - notify: () => {}, - mcp: { list: () => [], openAuthURL: () => {} }, + notify: () => undefined, + mcp: { list: () => [], openAuthURL: () => undefined }, }); expect(shell.overlayItems).toEqual([ "No MCP servers configured", @@ -934,8 +936,8 @@ describe("mcp surface", () => { test("the visible add row opens the same add-server flow", async () => { await withShell((shell) => { openCommandSurface(shell, "mcp", { - notify: () => {}, - mcp: { list: () => entries, openAuthURL: () => {} }, + notify: () => undefined, + mcp: { list: () => entries, openAuthURL: () => undefined }, }); moveOverlaySelection(shell, entries.length); acceptOverlaySelection(shell); @@ -947,10 +949,10 @@ describe("mcp surface", () => { await withShell((shell) => { const listeners = new Set<() => void>(); const deps: CommandSurfaceDeps = { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); @@ -980,10 +982,10 @@ describe("mcp surface", () => { }); const listeners = new Set<() => void>(); openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); @@ -1012,13 +1014,13 @@ describe("mcp surface", () => { for (const listener of [...listeners]) listener(); }; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => { listCalls += 1; return entries; }, - openAuthURL: () => {}, + openAuthURL: () => undefined, subscribe: (listener) => { listeners.add(listener); return () => { @@ -1047,10 +1049,10 @@ describe("mcp surface", () => { let liveEntries: readonly McpEntry[] = [{ name: "linear", state: "connecting" }]; const listeners = new Set<() => void>(); openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => liveEntries, - openAuthURL: () => {}, + openAuthURL: () => undefined, subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); @@ -1089,7 +1091,7 @@ describe("mcp surface", () => { const listeners = new Set<() => void>(); const opened: string[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => liveEntries, openAuthURL: (url) => opened.push(url), @@ -1121,7 +1123,7 @@ describe("mcp surface", () => { const opened: string[] = []; const retried: string[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, openAuthURL: (url) => opened.push(url), @@ -1155,7 +1157,7 @@ describe("mcp surface", () => { const opened: string[] = []; const retried: string[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => [entry], openAuthURL: (url) => opened.push(url), @@ -1226,10 +1228,10 @@ describe("mcp surface", () => { let unsubscribeCalls = 0; const added: { name: string; url: string }[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => [{ name: "sentry", state: "failed", error: "offline" }], - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); return { ok: true, message: "should not add" }; @@ -1261,7 +1263,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => [{ name: "sentry", state: "failed" as const, error: "offline" }], - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); return { @@ -1295,10 +1297,10 @@ describe("mcp surface", () => { const added: { name: string; url: string }[] = []; let liveEntries: readonly McpEntry[] = entries; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => liveEntries, - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); liveEntries = [...liveEntries, { name, state: "connecting" }]; @@ -1324,10 +1326,10 @@ describe("mcp surface", () => { await withWiredShell(async (shell, harness) => { const added: { name: string; url: string }[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); return { ok: true, message: "added" }; @@ -1360,10 +1362,10 @@ describe("mcp surface", () => { await withWiredShell(async (shell, harness) => { const added: { name: string; url: string }[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); return { ok: true, message: "added" }; @@ -1392,10 +1394,10 @@ describe("mcp surface", () => { }); let gateCancellations = 0; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: () => deferredAdd, }, }); @@ -1448,7 +1450,7 @@ describe("mcp surface", () => { }, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, subscribe: (listener) => { subscribeCalls += 1; listeners.add(listener); @@ -1505,7 +1507,7 @@ describe("mcp surface", () => { }, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, subscribe: (listener) => { subscribeCalls += 1; listeners.add(listener); @@ -1548,7 +1550,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); return { ok: true, message: "added" }; @@ -1584,7 +1586,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); return { ok: true, message: "added" }; @@ -1616,10 +1618,10 @@ describe("mcp surface", () => { await withShell((shell) => { const added: { name: string; url: string }[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, addServer: async (name, url) => { added.push({ name, url }); return { ok: true, message: "added" }; @@ -1637,8 +1639,8 @@ describe("mcp surface", () => { test("the mcp title advertises Alt+D and Alt+R, including when add is hidden", async () => { await withWiredShell(async (shell, harness) => { openCommandSurface(shell, "mcp", { - notify: () => {}, - mcp: { list: () => entries, openAuthURL: () => {} }, + notify: () => undefined, + mcp: { list: () => entries, openAuthURL: () => undefined }, }); await harness.renderOnce(); const withAdd = harness.captureCharFrame(); @@ -1647,10 +1649,10 @@ describe("mcp surface", () => { expect(withAdd).toContain("Alt+A"); openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => entries, - openAuthURL: () => {}, + openAuthURL: () => undefined, mcpServersSource: "local", }, }); @@ -1673,7 +1675,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => liveEntries, - openAuthURL: () => {}, + openAuthURL: () => undefined, setEnabled: async (name, enabled) => { toggled.push({ name, enabled }); liveEntries = [{ name, state: enabled ? "connecting" : "disabled" }]; @@ -1703,7 +1705,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => liveEntries, - openAuthURL: () => {}, + openAuthURL: () => undefined, setEnabled: async (name, enabled) => { toggled.push({ name, enabled }); liveEntries = [{ name, state: enabled ? "connecting" : "disabled" }]; @@ -1731,10 +1733,10 @@ describe("mcp surface", () => { { name: "notion", state: "connected", toolCount: 3 }, ]; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => liveEntries, - openAuthURL: () => {}, + openAuthURL: () => undefined, setEnabled: async (name, enabled) => { liveEntries = liveEntries.map((entry) => entry.name === name ? { name, state: enabled ? "connecting" : "disabled" } : entry, @@ -1772,10 +1774,10 @@ describe("mcp surface", () => { { name: "notion", state: "connected", toolCount: 3 }, ]; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => liveEntries, - openAuthURL: () => {}, + openAuthURL: () => undefined, removeServer: async (name) => { liveEntries = liveEntries.filter((entry) => entry.name !== name); return { ok: true, message: `Removed ${name}.` }; @@ -1801,7 +1803,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => [{ name: "linear", state: "connected", toolCount: 12 }], - openAuthURL: () => {}, + openAuthURL: () => undefined, setEnabled: async () => { throw new Error("disk is full"); }, @@ -1820,10 +1822,10 @@ describe("mcp surface", () => { test("disabled builtin Exa copy does not say Alt+D disables it", async () => { await withWiredShell(async (shell, harness) => { openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => [{ name: "exa", state: "disabled", builtin: true }], - openAuthURL: () => {}, + openAuthURL: () => undefined, }, }); await harness.renderOnce(); @@ -1845,7 +1847,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => liveEntries, - openAuthURL: () => {}, + openAuthURL: () => undefined, removeServer: async (name) => { removed.push(name); liveEntries = []; @@ -1873,10 +1875,10 @@ describe("mcp surface", () => { await withShell((shell) => { const removed: string[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => [{ name: "linear", state: "connected", toolCount: 12 }], - openAuthURL: () => {}, + openAuthURL: () => undefined, removeServer: async (name) => { removed.push(name); return { ok: true, message: `Removed ${name}.` }; @@ -1900,7 +1902,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => [{ name: "exa", state: "connected", toolCount: 1, builtin: true }], - openAuthURL: () => {}, + openAuthURL: () => undefined, removeServer: async (name) => { removed.push(name); return { ok: true, message: `Removed ${name}.` }; @@ -1921,7 +1923,7 @@ describe("mcp surface", () => { notify: (note) => notes.push(note), mcp: { list: () => [{ name: "exa", state: "disabled", builtin: true }], - openAuthURL: () => {}, + openAuthURL: () => undefined, }, }); expect(runOverlayAction(shell, altKey("r"))).toBe(true); @@ -1936,10 +1938,10 @@ describe("mcp surface", () => { const toggled: { name: string; enabled: boolean }[] = []; const removed: string[] = []; openCommandSurface(shell, "mcp", { - notify: () => {}, + notify: () => undefined, mcp: { list: () => [{ name: "linear", state: "connected", toolCount: 1 }], - openAuthURL: () => {}, + openAuthURL: () => undefined, setEnabled: async (name, enabled) => { toggled.push({ name, enabled }); return { ok: true, message: "should not run" }; @@ -1977,10 +1979,10 @@ describe("model surface", () => { await withShell((shell) => { let opened = 0; expect( - openCommandSurface(shell, "models", { notify: () => {}, openModels: () => opened++ }), + openCommandSurface(shell, "models", { notify: () => undefined, openModels: () => opened++ }), ).toBe(true); expect(opened).toBe(1); - expect(openCommandSurface(shell, "models", { notify: () => {} })).toBe(false); + expect(openCommandSurface(shell, "models", { notify: () => undefined })).toBe(false); }); }); }); @@ -1991,12 +1993,12 @@ describe("add-provider surface", () => { let opened = 0; expect( openCommandSurface(shell, "add-provider", { - notify: () => {}, + notify: () => undefined, openAddProvider: () => opened++, }), ).toBe(true); expect(opened).toBe(1); - expect(openCommandSurface(shell, "add-provider", { notify: () => {} })).toBe(false); + expect(openCommandSurface(shell, "add-provider", { notify: () => undefined })).toBe(false); }); }); }); @@ -2004,7 +2006,7 @@ describe("add-provider surface", () => { describe("help surface", () => { test("opens the keymap overlay", async () => { await withShell((shell) => { - expect(openCommandSurface(shell, "help", { notify: () => {} })).toBe(true); + expect(openCommandSurface(shell, "help", { notify: () => undefined })).toBe(true); expect(shell.overlayKind).toBe("help"); }); }); diff --git a/src/tui/commands/built-in.test.ts b/src/tui/commands/built-in.test.ts index 128cd6781..38c12bd10 100644 --- a/src/tui/commands/built-in.test.ts +++ b/src/tui/commands/built-in.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from "bun:test"; +import { defined } from "../../../tests/helpers/defined.js"; import { getCommand } from "./registry.js"; import type { CommandContext } from "./registry.js"; import { registerBuiltInCommands } from "./built-in.js"; @@ -7,7 +8,7 @@ import { buildCostSummary } from "../../cost/cost-summary.js"; registerBuiltInCommands(); const makeCtx = (): CommandContext => ({ - signalClear: () => {}, + signalClear: () => undefined, }); describe("/help command", () => { @@ -17,7 +18,7 @@ describe("/help command", () => { it("requests the help overlay", () => { const ctx = makeCtx(); - const result = getCommand("help")!.handler("", ctx); + const result = defined(getCommand("help"), "help").handler("", ctx); expect(result).toEqual({ type: "overlay", overlay: "help" }); }); }); @@ -47,7 +48,7 @@ describe("/connect command", () => { }); it("requests the add-provider overlay", () => { - expect(getCommand("connect")!.handler("", makeCtx())).toEqual({ + expect(defined(getCommand("connect"), "connect").handler("", makeCtx())).toEqual({ type: "overlay", overlay: "add-provider", }); @@ -64,17 +65,17 @@ describe("MCP commands", () => { describe("/status command", () => { it("answers from the live fleet without sending anything to the model", () => { const ctx: CommandContext = { - signalClear: () => {}, + signalClear: () => undefined, getFleetStatus: () => "2 running (api 1:20, docs 0:04) · 1 done", }; - expect(getCommand("status")!.handler("", ctx)).toEqual({ + expect(defined(getCommand("status"), "status").handler("", ctx)).toEqual({ type: "message", text: "2 running (api 1:20, docs 0:04) · 1 done", }); }); it("says so rather than throwing when no fleet source is wired", () => { - expect(getCommand("status")!.handler("", makeCtx())).toEqual({ + expect(defined(getCommand("status"), "status").handler("", makeCtx())).toEqual({ type: "message", text: "Fleet status is not available in this session.", }); @@ -95,23 +96,23 @@ describe("/yolo command", () => { it("toggles skip-permissions when invoked bare", () => { let skip = false; const ctx: CommandContext = { - signalClear: () => {}, + signalClear: () => undefined, getSkipPermissions: () => skip, setSkipPermissions: (value) => { skip = value; }, }; - expect(getCommand("yolo")!.handler("", ctx)).toEqual({ + expect(defined(getCommand("yolo"), "yolo").handler("", ctx)).toEqual({ type: "message", text: "Yolo mode on — permission prompts skipped. Saved as the default.", }); expect(skip).toBe(true); - expect(getCommand("yolo")!.handler("", ctx)).toEqual({ + expect(defined(getCommand("yolo"), "yolo").handler("", ctx)).toEqual({ type: "message", text: "Yolo mode off — permission prompts restored. Saved as the default.", }); expect(skip).toBe(false); - expect(getCommand("yolo")!.handler("toggle", ctx)).toEqual({ + expect(defined(getCommand("yolo"), "yolo").handler("toggle", ctx)).toEqual({ type: "message", text: "Yolo mode on — permission prompts skipped. Saved as the default.", }); @@ -121,18 +122,18 @@ describe("/yolo command", () => { it("turns skip-permissions on and off explicitly", () => { let skip = false; const ctx: CommandContext = { - signalClear: () => {}, + signalClear: () => undefined, getSkipPermissions: () => skip, setSkipPermissions: (value) => { skip = value; }, }; - expect(getCommand("yolo")!.handler("on", ctx)).toEqual({ + expect(defined(getCommand("yolo"), "yolo").handler("on", ctx)).toEqual({ type: "message", text: "Yolo mode on — permission prompts skipped. Saved as the default.", }); expect(skip).toBe(true); - expect(getCommand("yolo")!.handler("off", ctx)).toEqual({ + expect(defined(getCommand("yolo"), "yolo").handler("off", ctx)).toEqual({ type: "message", text: "Yolo mode off — permission prompts restored. Saved as the default.", }); @@ -141,18 +142,18 @@ describe("/yolo command", () => { it("rejects unknown arguments with usage", () => { const ctx: CommandContext = { - signalClear: () => {}, + signalClear: () => undefined, getSkipPermissions: () => false, - setSkipPermissions: () => {}, + setSkipPermissions: () => undefined, }; - expect(getCommand("yolo")!.handler("maybe", ctx)).toEqual({ + expect(defined(getCommand("yolo"), "yolo").handler("maybe", ctx)).toEqual({ type: "message", text: "Usage: /yolo [on|off|toggle]", }); }); it("says so when skip-permissions is not wired", () => { - expect(getCommand("yolo")!.handler("", makeCtx())).toEqual({ + expect(defined(getCommand("yolo"), "yolo").handler("", makeCtx())).toEqual({ type: "message", text: "Yolo mode is not available in this mode.", }); @@ -165,7 +166,7 @@ describe("/model command", () => { }); it("opens the agent configuration modal", () => { - expect(getCommand("model")!.handler("", makeCtx())).toEqual({ type: "modal", modal: "agent" }); + expect(defined(getCommand("model"), "model").handler("", makeCtx())).toEqual({ type: "modal", modal: "agent" }); }); it("/agent alias is not registered", () => { @@ -176,7 +177,7 @@ describe("/model command", () => { describe("/clear command", () => { it("returns a local message and does not send to the agent", () => { const ctx = makeCtx(); - const result = getCommand("clear")!.handler("", ctx); + const result = defined(getCommand("clear"), "clear").handler("", ctx); expect(result).toEqual({ type: "message", text: "Started a fresh session." }); }); @@ -186,7 +187,7 @@ describe("/clear command", () => { ctx.signalClear = () => { called = true; }; - getCommand("clear")!.handler("", ctx); + defined(getCommand("clear"), "clear").handler("", ctx); expect(called).toBe(true); }); }); @@ -194,7 +195,7 @@ describe("/clear command", () => { describe("/new command", () => { it("returns a local message and does not send to the agent", () => { const ctx = makeCtx(); - const result = getCommand("new")!.handler("", ctx); + const result = defined(getCommand("new"), "new").handler("", ctx); expect(result).toEqual({ type: "message", text: "Started a fresh session." }); }); @@ -204,7 +205,7 @@ describe("/new command", () => { ctx.signalClear = () => { called = true; }; - getCommand("new")!.handler("", ctx); + defined(getCommand("new"), "new").handler("", ctx); expect(called).toBe(true); }); }); @@ -219,7 +220,7 @@ describe("removed tier commands", () => { describe("/cost command", () => { it("reports unavailable when the session supplies no summary", () => { - const result = getCommand("cost")!.handler("", makeCtx()); + const result = defined(getCommand("cost"), "cost").handler("", makeCtx()); expect(result).toEqual({ type: "message", text: "Cost tracking is not available in this session.", @@ -240,7 +241,7 @@ describe("/cost command", () => { contextTokens: 160, contextIsEstimate: false, }); - const result = getCommand("cost")!.handler("", ctx); + const result = defined(getCommand("cost"), "cost").handler("", ctx); expect(result.type).toBe("message"); expect((result as { text: string }).text).toContain("Model: claude-x"); expect((result as { text: string }).text).toContain("Cost: $0.4200"); @@ -255,12 +256,12 @@ describe("/feedback command", () => { it("arms multi-turn capture when invoked bare", () => { let armed = false; const ctx: CommandContext = { - signalClear: () => {}, + signalClear: () => undefined, beginFeedbackCapture: () => { armed = true; }, }; - expect(getCommand("feedback")!.handler("", ctx)).toEqual({ + expect(defined(getCommand("feedback"), "feedback").handler("", ctx)).toEqual({ type: "message", text: "Please share your feedback. When done please hit enter. (Empty Enter cancels.)", }); @@ -270,13 +271,13 @@ describe("/feedback command", () => { it("submits inline text immediately", () => { const sent: string[] = []; const ctx: CommandContext = { - signalClear: () => {}, + signalClear: () => undefined, submitFeedback: (text) => { sent.push(text); return "Thanks — feedback sent."; }, }; - expect(getCommand("feedback")!.handler("love the TUI", ctx)).toEqual({ + expect(defined(getCommand("feedback"), "feedback").handler("love the TUI", ctx)).toEqual({ type: "message", text: "Thanks — feedback sent.", }); @@ -284,14 +285,14 @@ describe("/feedback command", () => { }); it("fails closed for bare /feedback when capture is not wired", () => { - expect(getCommand("feedback")!.handler("", makeCtx())).toEqual({ + expect(defined(getCommand("feedback"), "feedback").handler("", makeCtx())).toEqual({ type: "message", text: "Feedback is not available in this mode.", }); }); it("explains when the feedback path is not wired", () => { - expect(getCommand("feedback")!.handler("x", makeCtx())).toEqual({ + expect(defined(getCommand("feedback"), "feedback").handler("x", makeCtx())).toEqual({ type: "message", text: "Feedback is not available in this mode.", }); diff --git a/src/tui/commands/registry.test.ts b/src/tui/commands/registry.test.ts index ef4d319ce..926b5c227 100644 --- a/src/tui/commands/registry.test.ts +++ b/src/tui/commands/registry.test.ts @@ -10,7 +10,7 @@ import { import type { CommandContext } from "./registry.js"; const ctx: CommandContext = { - signalClear: () => {}, + signalClear: () => undefined, }; afterEach(() => { diff --git a/src/tui/copy-path.test.ts b/src/tui/copy-path.test.ts index 854a2efa1..f94e7b277 100644 --- a/src/tui/copy-path.test.ts +++ b/src/tui/copy-path.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { buildCopyTargets, classifyCopy, @@ -65,7 +66,7 @@ describe("writeClipboard", () => { }); test("async resolve defers onSuccess", async () => { - let resolveWrite!: () => void; + let resolveWrite: () => void = () => undefined; const writeP = new Promise((r) => { resolveWrite = r; }); @@ -97,19 +98,18 @@ describe("writeClipboard", () => { describe("formatCopyText / copyStreamRow", () => { test("writes plain text and summary", () => { const port = createRecordingClipboard(); - const payload = copyStreamRow({ role: "assistant", text: "hello world" }, port); - expect(payload).not.toBeNull(); - expect(payload!.kind).toBe("message"); - expect(payload!.text).toBe("hello world"); + const payload = defined(copyStreamRow({ role: "assistant", text: "hello world" }, port)); + expect(payload.kind).toBe("message"); + expect(payload.text).toBe("hello world"); expect(port.writes).toEqual(["hello world"]); - expect(payload!.summary).toContain("copied message"); + expect(payload.summary).toContain("copied message"); }); test("tool includes meta", () => { const port = createRecordingClipboard(); - const payload = copyStreamRow({ role: "tool", text: "ok", meta: "bash" }, port); - expect(payload!.text).toBe("[bash] ok"); - expect(payload!.kind).toBe("tool"); + const payload = defined(copyStreamRow({ role: "tool", text: "ok", meta: "bash" }, port)); + expect(payload.text).toBe("[bash] ok"); + expect(payload.kind).toBe("tool"); }); test("null when no row", () => { diff --git a/src/tui/copy-path.ts b/src/tui/copy-path.ts index 9c8df1ba4..14a1c1ec4 100644 --- a/src/tui/copy-path.ts +++ b/src/tui/copy-path.ts @@ -170,7 +170,7 @@ export function copyStreamRow( if (!row) return null; const payload = formatCopyText(row); writeClipboard(port, payload.text, { - onSuccess: () => {}, + onSuccess: () => undefined, }); return payload; } diff --git a/src/tui/copy-wire.test.ts b/src/tui/copy-wire.test.ts index 20fefb384..120600f75 100644 --- a/src/tui/copy-wire.test.ts +++ b/src/tui/copy-wire.test.ts @@ -26,12 +26,12 @@ function capturingSchedule(lapse: (() => void)[], expectedMs = RUNTIME_FLASH_MS) return (fn, ms) => { expect(ms).toBe(expectedMs); lapse.push(fn); - return () => {}; + return () => undefined; }; } /** Do not arm a real timer: bun test runs files in one process. */ -const ignoreExpiry: FlashSchedule = () => () => {}; +const ignoreExpiry: FlashSchedule = () => () => undefined; describe("Alt+C reaches the injected clipboard", () => { test("confirming a copy target writes its text", () => { diff --git a/src/tui/decision-truncation.test.ts b/src/tui/decision-truncation.test.ts index 37c6e700c..8948c5c77 100644 --- a/src/tui/decision-truncation.test.ts +++ b/src/tui/decision-truncation.test.ts @@ -92,7 +92,7 @@ describe("decision choice rendering", () => { emitter.emit("permission.gate", { id: "req-1", request: hintRequest, - resolve: () => {}, + resolve: () => undefined, }); try { diff --git a/src/tui/diff-rows.test.ts b/src/tui/diff-rows.test.ts index 95513e405..f0561a8a0 100644 --- a/src/tui/diff-rows.test.ts +++ b/src/tui/diff-rows.test.ts @@ -5,6 +5,7 @@ import { describe, expect, test } from "bun:test"; import { rgbToHex, type CapturedSpan } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { toolCallRow } from "./diff"; import { withTestRenderer, type Harness } from "./harness"; @@ -97,7 +98,7 @@ describe("diff transcript rows", () => { expect(changedRemoved?.fg).toBe(DIFF_FG.del); expect(changedAdded?.fg).toBe(DIFF_FG.add); // Bold attribute distinguishes the changed tokens inside the line. - expect(changedRemoved!.attributes).toBeGreaterThan(0); + expect(defined(changedRemoved).attributes).toBeGreaterThan(0); // "const" is shared by both sides, so it stays in the context tone. expect(shared.length).toBeGreaterThan(0); expect(shared.every((s) => s.fg === DIFF_FG.context)).toBe(true); @@ -166,7 +167,7 @@ describe("diff transcript rows", () => { }); const row = toolCallRow({ name: "spawn_agent", arguments: args }); expect(row.summary).toBeDefined(); - expect(row.summary!.length).toBeGreaterThan(0); + expect(defined(row.summary).length).toBeGreaterThan(0); expect(row.summary).not.toContain("success_criteria"); expect(row.summary).not.toContain('"intent"'); // Paint layer must not fall through to raw text. diff --git a/src/tui/diff.test.ts b/src/tui/diff.test.ts index 45f8ca7c9..9a7e0da45 100644 --- a/src/tui/diff.test.ts +++ b/src/tui/diff.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { diffLines, diffPlainText, @@ -76,8 +77,8 @@ describe("renderDiff", () => { test("word-level LCS keeps shared tokens as context and paints only the delta", () => { const lines = renderDiff("const foo = bar(x, y);", "const foo = baz(x, y);", 80); - const delBody = lines[0]!.slice(2); - const addBody = lines[1]!.slice(2); + const delBody = defined(lines[0]).slice(2); + const addBody = defined(lines[1]).slice(2); const delChanged = delBody .filter((s) => s.fg === DIFF_FG.del) .map((s) => s.text) @@ -95,14 +96,14 @@ describe("renderDiff", () => { test("changed intra-line tokens are bold; shared tokens are not", () => { const lines = renderDiff("a b c", "a x c", 40); - const delBody = lines[0]!.slice(2); + const delBody = defined(lines[0]).slice(2); expect(delBody.find((s) => s.text === "b")?.bold).toBe(true); expect(delBody.find((s) => s.text === "a")?.bold).toBeUndefined(); }); test("word-level LCS is not positional — reordered shared words stay context", () => { const lines = renderDiff("a b c", "a x c", 40); - const changed = lines[0]! + const changed = defined(lines[0]) .slice(2) .filter((s) => s.fg === DIFF_FG.del) .map((s) => s.text.trim()) @@ -113,18 +114,18 @@ describe("renderDiff", () => { test("unpaired adds and removals take the add/remove tone whole-line", () => { const [removed] = renderDiff("old line", "", 40); const [added] = renderDiff("", "new line", 40); - expect(removed!.slice(1).every((s) => s.fg === DIFF_FG.del)).toBe(true); - expect(added!.slice(1).every((s) => s.fg === DIFF_FG.add)).toBe(true); + expect(defined(removed).slice(1).every((s) => s.fg === DIFF_FG.del)).toBe(true); + expect(defined(added).slice(1).every((s) => s.fg === DIFF_FG.add)).toBe(true); }); test("context rows take the muted context tone", () => { const lines = renderDiff("a\nb", "a\nB", 40); - expect(lines[0]!.every((s) => s.fg === DIFF_FG.context)).toBe(true); + expect(defined(lines[0]).every((s) => s.fg === DIFF_FG.context)).toBe(true); }); test("line-number column always uses the muted context tone", () => { const lines = renderDiff("a\nb", "a\nB", 40); - expect(lines.map((line) => line[0]!.fg)).toEqual([ + expect(lines.map((line) => defined(line[0]).fg)).toEqual([ DIFF_FG.context, DIFF_FG.context, DIFF_FG.context, @@ -135,25 +136,25 @@ describe("renderDiff", () => { const long = Array.from({ length: 30 }, (_, i) => `w${i}`).join(" "); const lines = renderDiff("", long, 30, { lineNumbers: false }); expect(lines.length).toBeGreaterThan(1); - expect(lines[0]![0]!.text).toBe("+ "); + expect(defined(defined(lines[0])[0]).text).toBe("+ "); // Continuation rows blank the sign column rather than repeating it. - expect(lines[1]![0]!.text).toBe(" "); + expect(defined(defined(lines[1])[0]).text).toBe(" "); }); }); describe("renderDiff line numbers", () => { test("context rows carry both old and new line numbers", () => { const lines = renderDiff("a\nb\nc", "a\nB\nc", 40); - expect(textOf(lines[0]!)).toContain("1 1"); - expect(textOf(lines[1]!)).toContain("2 "); - expect(textOf(lines[2]!)).toContain(" 2"); - expect(textOf(lines[3]!)).toContain("3 3"); + expect(textOf(defined(lines[0]))).toContain("1 1"); + expect(textOf(defined(lines[1]))).toContain("2 "); + expect(textOf(defined(lines[2]))).toContain(" 2"); + expect(textOf(defined(lines[3]))).toContain("3 3"); }); test("del rows show only the old number, add rows show only the new number", () => { const lines = renderDiff("old", "new", 40); - expect(lines[0]![0]!.text).toBe("1 "); - expect(lines[1]![0]!.text).toBe(" 1 "); + expect(defined(defined(lines[0])[0]).text).toBe("1 "); + expect(defined(defined(lines[1])[0]).text).toBe(" 1 "); }); test("lineNumbers: false omits the number gutter entirely", () => { @@ -164,7 +165,7 @@ describe("renderDiff line numbers", () => { test("line numbers stay right-aligned as the file grows past one digit", () => { const oldText = Array.from({ length: 12 }, (_, i) => `line ${i}`).join("\n"); const newText = oldText.replace("line 0", "CHANGED"); - const widths = new Set(renderDiff(oldText, newText, 80).map((line) => line[0]!.text.length)); + const widths = new Set(renderDiff(oldText, newText, 80).map((line) => defined(line[0]).text.length)); expect(widths.size).toBe(1); }); @@ -174,7 +175,7 @@ describe("renderDiff line numbers", () => { const lines = renderDiff(oldText, newText, 40, { contextLines: 2 }); const marker = lines.find((line) => textOf(line).includes("unchanged line")); expect(marker).toBeDefined(); - expect(marker![0]!.text.trim()).toBe(""); + expect(defined(defined(marker)[0]).text.trim()).toBe(""); }); }); @@ -186,13 +187,12 @@ describe("editDiffView", () => { }); test("builds a numberless diff with stats and path for edit_file", () => { - const view = editDiffView("edit_file", EDIT_ARGS); - expect(view).not.toBeNull(); - expect(view!.added).toBe(1); - expect(view!.removed).toBe(1); - expect(view!.path).toBe("src/x.ts"); + const view = defined(editDiffView("edit_file", EDIT_ARGS)); + expect(view.added).toBe(1); + expect(view.removed).toBe(1); + expect(view.path).toBe("src/x.ts"); // Snippet-relative numbers would be misleading, so they are suppressed. - expect(view!.lines.map(textOf)).toEqual(["- const a = 1", "+ const a = 2"]); + expect(view.lines.map(textOf)).toEqual(["- const a = 1", "+ const a = 2"]); }); test("returns null for non-edit tools and no-op edits", () => { @@ -204,10 +204,10 @@ describe("editDiffView", () => { test("caps a huge write_file body with a truncation marker", () => { const content = Array.from({ length: 200 }, (_, i) => `line ${i}`).join("\n"); - const view = editDiffView("write_file", JSON.stringify({ content })); - expect(view!.added).toBe(200); - expect(view!.lines.length).toBeLessThan(200); - expect(textOf(view!.lines.at(-1)!)).toContain("more diff lines"); + const view = defined(editDiffView("write_file", JSON.stringify({ content }))); + expect(view.added).toBe(200); + expect(view.lines.length).toBeLessThan(200); + expect(textOf(defined(view.lines.at(-1)))).toContain("more diff lines"); }); }); @@ -246,6 +246,6 @@ describe("toolCallRow", () => { describe("diffPlainText", () => { test("joins segment text back into a copyable body", () => { const view = editDiffView("edit_file", JSON.stringify({ old_string: "a", new_string: "b" })); - expect(diffPlainText(view!)).toBe("- a\n+ b"); + expect(diffPlainText(defined(view))).toBe("- a\n+ b"); }); }); diff --git a/src/tui/diff.ts b/src/tui/diff.ts index acf192c46..f3e453478 100644 --- a/src/tui/diff.ts +++ b/src/tui/diff.ts @@ -52,14 +52,32 @@ type NumberedRow = DiffRow & { collapsed?: boolean; }; +function lcsCell(table: number[][], i: number, j: number): number { + const row = table[i]; + if (row == null) throw new Error("lcs table row missing"); + const value = row[j]; + if (value == null) throw new Error("lcs table cell missing"); + return value; +} + +function requireDiffLine(lines: readonly string[], index: number): string { + const line = lines[index]; + if (line == null) throw new Error("diff line missing"); + return line; +} + function lcsTable(a: readonly string[], b: readonly string[]): number[][] { const n = a.length; const m = b.length; const table: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); for (let i = n - 1; i >= 0; i--) { for (let j = m - 1; j >= 0; j--) { - table[i]![j] = - a[i] === b[j] ? table[i + 1]![j + 1]! + 1 : Math.max(table[i + 1]![j]!, table[i]![j + 1]!); + const row = table[i]; + if (row == null) throw new Error("lcs table row missing"); + row[j] = + a[i] === b[j] + ? lcsCell(table, i + 1, j + 1) + 1 + : Math.max(lcsCell(table, i + 1, j), lcsCell(table, i, j + 1)); } } return table; @@ -83,19 +101,19 @@ export function diffLines(oldText: string, newText: string): DiffRow[] { let j = 0; while (i < n && j < m) { if (a[i] === b[j]) { - rows.push({ kind: "context", text: a[i]! }); + rows.push({ kind: "context", text: requireDiffLine(a, i) }); i++; j++; - } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) { - rows.push({ kind: "del", text: a[i]! }); + } else if (lcsCell(lcs, i + 1, j) >= lcsCell(lcs, i, j + 1)) { + rows.push({ kind: "del", text: requireDiffLine(a, i) }); i++; } else { - rows.push({ kind: "add", text: b[j]! }); + rows.push({ kind: "add", text: requireDiffLine(b, j) }); j++; } } - while (i < n) rows.push({ kind: "del", text: a[i++]! }); - while (j < m) rows.push({ kind: "add", text: b[j++]! }); + while (i < n) rows.push({ kind: "del", text: requireDiffLine(a, i++) }); + while (j < m) rows.push({ kind: "add", text: requireDiffLine(b, j++) }); return rows; } @@ -197,17 +215,17 @@ export function wordDiffSegments(line: string, kind: "add" | "del", paired: stri const m = other.length; while (i < n && j < m) { if (self[i] === other[j]) { - out.push({ text: self[i]!, fg: DIFF_FG.context }); + out.push({ text: requireDiffLine(self, i), fg: DIFF_FG.context }); i++; j++; - } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) { - out.push(changed(self[i]!)); + } else if (lcsCell(lcs, i + 1, j) >= lcsCell(lcs, i, j + 1)) { + out.push(changed(requireDiffLine(self, i))); i++; } else { j++; } } - while (i < n) out.push(changed(self[i++]!)); + while (i < n) out.push(changed(requireDiffLine(self, i++))); return out.length > 0 ? out : [changed(line)]; } @@ -272,17 +290,20 @@ export function renderDiff( const lines: DiffLine[] = []; const bodyWidth = Math.max(1, width - numColWidth - 2); for (let r = 0; r < rows.length; r++) { - const row = rows[r]!; + const row = rows[r]; + if (row == null) throw new Error("diff row missing"); const numCol = row.collapsed === true ? " ".repeat(numColWidth) : `${padNum(row.oldNum, numWidth)} ${padNum(row.newNum, numWidth)} `; const sign = GUTTER[row.kind]; + const next = rows[r + 1]; + const prev = rows[r - 1]; const paired = - row.kind === "del" && rows[r + 1]?.kind === "add" - ? rows[r + 1]!.text - : row.kind === "add" && rows[r - 1]?.kind === "del" - ? rows[r - 1]!.text + row.kind === "del" && next?.kind === "add" + ? next.text + : row.kind === "add" && prev?.kind === "del" + ? prev.text : undefined; const segFg = rowColor(row.kind); const bodySegs: DiffSegment[] = @@ -292,7 +313,8 @@ export function renderDiff( const ranges = row.text.length === 0 ? [{ start: 0, end: 0 }] : wrapRanges(row.text, bodyWidth); for (let idx = 0; idx < ranges.length; idx++) { - const range = ranges[idx]!; + const range = ranges[idx]; + if (range == null) throw new Error("diff wrap range missing"); const piece = sliceSegments(bodySegs, range.start, range.end); lines.push([ ...(showNumbers diff --git a/src/tui/focus/focus-state.test.ts b/src/tui/focus/focus-state.test.ts index fdbf752bd..95d8567b7 100644 --- a/src/tui/focus/focus-state.test.ts +++ b/src/tui/focus/focus-state.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../../tests/helpers/defined.js"; import { canPopFocus, createFocusState, @@ -17,7 +18,7 @@ describe("createFocusState", () => { expect(focusOwner(s)).toBe("prompt"); expect(scrollLease(s)).toBe("transcript"); expect(s.frames).toHaveLength(1); - expect(s.frames[0]!.id).toBe("shell"); + expect(defined(s.frames[0]).id).toBe("shell"); }); }); @@ -39,7 +40,7 @@ describe("one focus owner + one scroll lease", () => { expect(scrollLease(s)).not.toBeNull(); // Single top frame owns both; stack never empty. expect(s.frames.length).toBeGreaterThanOrEqual(1); - expect(scrollLease(s)).toBe(s.frames[s.frames.length - 1]!.scrollOwner); + expect(scrollLease(s)).toBe(defined(s.frames[s.frames.length - 1]).scrollOwner); } }); diff --git a/src/tui/focus/focus-state.ts b/src/tui/focus/focus-state.ts index ed2a6de11..d483ff18e 100644 --- a/src/tui/focus/focus-state.ts +++ b/src/tui/focus/focus-state.ts @@ -103,7 +103,8 @@ export function popFocus(state: FocusState): FocusState { if (state.frames.length > 1) { const next = state.frames.slice(0, -1); // Left observe (or last stacked surface): shell is sole frame → prompt + transcript. - if (next.length === 1 && next[0]!.id === SHELL_ID) { + const frame = next[0]; + if (next.length === 1 && frame != null && frame.id === SHELL_ID) { return { frames: [shellFrame("prompt", "transcript")] }; } // Popped overlay/palette above observe (or another overlay): restore as recorded. diff --git a/src/tui/gate-wire.test.ts b/src/tui/gate-wire.test.ts index 8c35fc218..26091c190 100644 --- a/src/tui/gate-wire.test.ts +++ b/src/tui/gate-wire.test.ts @@ -352,7 +352,7 @@ describe("wireGates", () => { }; try { const dispose = wireGates(emitter, shell); - emitter.emit("permission.gate", { id: "req-1", request, resolve: () => {} }); + emitter.emit("permission.gate", { id: "req-1", request, resolve: () => undefined }); const collapsed = shell.overlayBodyLines.join("\n"); expect(collapsed).toContain("1) echo start"); @@ -729,7 +729,7 @@ describe("wireGates", () => { }; try { const dispose = wireGates(emitter, shell); - emitter.emit("permission.gate", { id: "req-1", request, resolve: () => {} }); + emitter.emit("permission.gate", { id: "req-1", request, resolve: () => undefined }); expect(shell.streamLog.filter((r) => r.meta === "permission")).toHaveLength(0); @@ -755,7 +755,7 @@ describe("gate decisions stay out of the transcript", () => { const emitter = new EventEmitter(); try { wireGates(emitter, shell); - emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), resolve: () => {} }); + emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), resolve: () => undefined }); const before = shell.streamLog.length; acceptOverlaySelection(shell); @@ -775,7 +775,7 @@ describe("gate decisions stay out of the transcript", () => { const emitter = new EventEmitter(); try { wireGates(emitter, shell); - emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), resolve: () => {} }); + emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), resolve: () => undefined }); const before = shell.streamLog.length; closeInsetOverlay(shell); @@ -799,7 +799,7 @@ describe("gate decisions stay out of the transcript", () => { id: "ask-1", question: "Proceed?", options: ["Cancel", "Continue"], - resolve: () => {}, + resolve: () => undefined, }); const before = shell.streamLog.length; @@ -824,7 +824,7 @@ describe("gate decisions stay out of the transcript", () => { id: "ask-1", question: "Proceed?", options: ["Cancel", "Continue"], - resolve: () => {}, + resolve: () => undefined, }); const before = shell.streamLog.length; @@ -849,7 +849,7 @@ describe("gate decisions stay out of the transcript", () => { id: "ask-1", question: "Proceed?", options: ["Cancel", "Continue"], - resolve: () => {}, + resolve: () => undefined, }); setOverlayAnswerActive(shell, true); @@ -890,7 +890,7 @@ describe("gate decisions stay out of the transcript", () => { emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), - resolve: () => {}, + resolve: () => undefined, timeoutMs: 5, }); await new Promise((r) => setTimeout(r, 20)); @@ -915,7 +915,7 @@ describe("gate decisions stay out of the transcript", () => { emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), - resolve: () => {}, + resolve: () => undefined, signal: controller.signal, }); controller.abort(); @@ -979,7 +979,7 @@ describe("gate decisions stay out of the transcript", () => { emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), - resolve: () => {}, + resolve: () => undefined, }); const before = shell.streamLog.length; emitter.emit("permission.gate", { @@ -1028,7 +1028,7 @@ describe("gate decisions stay out of the transcript", () => { emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), - resolve: () => {}, + resolve: () => undefined, }); const before = shell.streamLog.length; emitter.emit("permission.gate", { @@ -1411,7 +1411,7 @@ describe("operator.gate auto-cancel", () => { emitter.emit("permission.gate", { id: "req-1", request: baseRequest(), - resolve: () => {}, + resolve: () => undefined, }); emitter.emit("operator.gate", { id: "ask-1", @@ -1530,7 +1530,7 @@ describe("operator.gate auto-cancel", () => { id: "ask-1", question: "Proceed?", options: ["Yes", "No"], - resolve: () => {}, + resolve: () => undefined, timeoutMs: 5, }); await new Promise((r) => setTimeout(r, 20)); @@ -1669,7 +1669,7 @@ describe("permission overlay height", () => { pattern: `p${i}`, })), }, - resolve: () => {}, + resolve: () => undefined, }); }; diff --git a/src/tui/gate-wire.ts b/src/tui/gate-wire.ts index 169a11ef7..659581051 100644 --- a/src/tui/gate-wire.ts +++ b/src/tui/gate-wire.ts @@ -211,8 +211,8 @@ export interface GateLifecycleHooks { } const NOOP_GATE_HOOKS: GateLifecycleHooks = { - onGateOpened: () => {}, - onGateClosed: () => {}, + onGateOpened: () => undefined, + onGateClosed: () => undefined, }; /** diff --git a/src/tui/geometry.test.ts b/src/tui/geometry.test.ts index 548d3c024..352e122d3 100644 --- a/src/tui/geometry.test.ts +++ b/src/tui/geometry.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { AGENTS_PANEL_MAX_VISIBLE, COLLAPSE_ORDER, @@ -84,13 +85,12 @@ describe("resolveGeometry — 80×24 idle floor", () => { expect(layout.contentWidth).toBe(80 - SIDE_MARGIN * 2); let y = 0; for (const id of order) { - const r = layout.regions[id]; - expect(r).toBeDefined(); - expect(r!.x).toBe(layout.sideMargin); - expect(r!.width).toBe(layout.contentWidth); - expect(r!.y).toBe(y); - expect(r!.height).toBeGreaterThan(0); - y += r!.height; + const r = defined(layout.regions[id], id); + expect(r.x).toBe(layout.sideMargin); + expect(r.width).toBe(layout.contentWidth); + expect(r.y).toBe(y); + expect(r.height).toBeGreaterThan(0); + y += r.height; } expect(y).toBe(24); }); @@ -148,7 +148,7 @@ describe("resolveGeometry — agents panel", () => { expect(tall.heights.agents).toBe(requested); expect(tall.regions.agents?.width).toBe(tall.contentWidth); // Stack: agents sit below transcript and consume vertical chrome. - expect(tall.regions.agents!.y).toBeGreaterThan(tall.regions.transcript!.y); + expect(defined(tall.regions.agents).y).toBeGreaterThan(defined(tall.regions.transcript).y); }); test("with a fleet running the agents zone stacks under the transcript", () => { @@ -226,17 +226,13 @@ describe("resolveGeometry — task panel", () => { const layout = idle80x24({ visibility: { task: 3, agents: 1 }, }); - const transcript = layout.regions.transcript; - const agents = layout.regions.agents; - const task = layout.regions.task; - const prompt = layout.regions.prompt; - expect(transcript).toBeDefined(); - expect(agents).toBeDefined(); - expect(task).toBeDefined(); - expect(prompt).toBeDefined(); - expect(transcript!.y).toBeLessThan(agents!.y); - expect(agents!.y).toBeLessThan(task!.y); - expect(task!.y).toBeLessThan(prompt!.y); + const transcript = defined(layout.regions.transcript); + const agents = defined(layout.regions.agents); + const task = defined(layout.regions.task); + const prompt = defined(layout.regions.prompt); + expect(transcript.y).toBeLessThan(agents.y); + expect(agents.y).toBeLessThan(task.y); + expect(task.y).toBeLessThan(prompt.y); }); test("under pressure the task panel shrinks one row at a time rather than vanishing in one step", () => { @@ -481,20 +477,17 @@ describe("resolveGeometry — stack-only layout", () => { expect(layout.railGutter).toBe(0); expect(layout.chatWidth).toBe(layout.contentWidth); - const transcript = layout.regions.transcript; - const agents = layout.regions.agents; - const prompt = layout.regions.prompt; - expect(transcript).toBeDefined(); - expect(agents).toBeDefined(); - expect(prompt).toBeDefined(); + const transcript = defined(layout.regions.transcript); + const agents = defined(layout.regions.agents); + const prompt = defined(layout.regions.prompt); // Agents strip sits below transcript, full content width. - expect(agents!.y).toBeGreaterThan(transcript!.y); - expect(transcript!.width).toBe(layout.contentWidth); - expect(agents!.width).toBe(layout.contentWidth); - expect(agents!.height).toBe(layout.heights.agents); - expect(prompt!.width).toBe(layout.contentWidth); - expect(prompt!.x).toBe(layout.sideMargin); + expect(agents.y).toBeGreaterThan(transcript.y); + expect(transcript.width).toBe(layout.contentWidth); + expect(agents.width).toBe(layout.contentWidth); + expect(agents.height).toBe(layout.heights.agents); + expect(prompt.width).toBe(layout.contentWidth); + expect(prompt.x).toBe(layout.sideMargin); }); test("agents height reduces transcript vs idle baseline (stack chrome)", () => { @@ -524,7 +517,7 @@ describe("resolveGeometry — stack-only layout", () => { expect(layout.chatWidth).toBe(layout.contentWidth); expect(layout.regions.transcript?.width).toBe(layout.contentWidth); expect(layout.regions.agents?.width).toBe(layout.contentWidth); - expect(layout.regions.agents!.y).toBeGreaterThan(layout.regions.transcript!.y); + expect(defined(layout.regions.agents).y).toBeGreaterThan(defined(layout.regions.transcript).y); expect(layout.chromeHeight).toBeGreaterThan(PROMPT_IDLE_ROWS); }); diff --git a/src/tui/harness.test.ts b/src/tui/harness.test.ts index e3c562036..286339e9e 100644 --- a/src/tui/harness.test.ts +++ b/src/tui/harness.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { BoxRenderable, TextRenderable, type KeyEvent } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { createHarness, withTestRenderer } from "./harness.js"; describe("withTestRenderer", () => { @@ -35,25 +36,22 @@ describe("withTestRenderer", () => { h.pressKey("Enter"); await h.renderOnce(); - const enter = captured.at(-1); - expect(enter).toBeDefined(); - expect(enter!.name === "return" || enter!.name === "enter").toBe(true); - expect(enter!.ctrl).toBe(false); - expect(enter!.meta).toBe(false); + const enter = defined(captured.at(-1), "enter"); + expect(enter.name === "return" || enter.name === "enter").toBe(true); + expect(enter.ctrl).toBe(false); + expect(enter.meta).toBe(false); h.pressKey("Alt+Enter"); await h.renderOnce(); - const altEnter = captured.at(-1); - expect(altEnter).toBeDefined(); - expect(altEnter!.name === "return" || altEnter!.name === "enter").toBe(true); - expect(altEnter!.meta === true || altEnter!.option === true).toBe(true); + const altEnter = defined(captured.at(-1), "altEnter"); + expect(altEnter.name === "return" || altEnter.name === "enter").toBe(true); + expect(altEnter.meta === true || altEnter.option === true).toBe(true); h.pressKey("Ctrl+C"); await h.renderOnce(); - const ctrlC = captured.at(-1); - expect(ctrlC).toBeDefined(); - expect(ctrlC!.name).toBe("c"); - expect(ctrlC!.ctrl).toBe(true); + const ctrlC = defined(captured.at(-1), "ctrlC"); + expect(ctrlC.name).toBe("c"); + expect(ctrlC.ctrl).toBe(true); }); }); }); diff --git a/src/tui/image-attachments.test.ts b/src/tui/image-attachments.test.ts index 4778804d4..3e9a8691d 100644 --- a/src/tui/image-attachments.test.ts +++ b/src/tui/image-attachments.test.ts @@ -3,6 +3,7 @@ import { deflateSync } from "node:zlib"; import { unlink } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { defined } from "../../tests/helpers/defined.js"; import { findDuplicateAttachment, findImagePathMentions, @@ -29,7 +30,7 @@ const CRC_TABLE = (() => { function crc32(buf: Buffer): number { let c = 0xffffffff; - for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 0xff]! ^ (c >>> 8); + for (const byte of buf) c = defined(CRC_TABLE[(c ^ byte) & 0xff]) ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } diff --git a/src/tui/keybindings.test.ts b/src/tui/keybindings.test.ts index c00599222..ffcdf9842 100644 --- a/src/tui/keybindings.test.ts +++ b/src/tui/keybindings.test.ts @@ -16,6 +16,7 @@ import { EventEmitter } from "node:events"; import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { PROMPT_KEY_BINDINGS } from "./prompt-input.js"; import { helpItems, SHELL_SHORTCUTS } from "./keybindings.js"; import { createHarness, withTestRenderer, type Harness } from "./harness.js"; @@ -483,7 +484,7 @@ const PROBES: Readonly {}, + onSubmit: () => undefined, onInterrupt: () => { interrupted++; }, @@ -510,7 +511,7 @@ const PROBES: Readonly row.meta); // The retracted message's row is rewritten, not left claiming "queue" // as though it will still dispatch (the bug that got the first attempt @@ -620,7 +621,7 @@ async function settle(h: Harness): Promise { } function attachOnNextPrompt(shell: AppShell, id: string): Promise { - let done: () => void = () => {}; + let done: () => void = () => undefined; const attached = new Promise((resolve) => { done = resolve; }); @@ -644,7 +645,7 @@ function recordSubmits(shell: AppShell): { readonly text: string; readonly kind: const sent: { text: string; kind: string }[] = []; setShellBridgeHooks(shell, { onSubmit: (text, kind) => sent.push({ text, kind }), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); return sent; @@ -724,15 +725,15 @@ describe("the runner host does not shadow the prompt bindings the catalog claims const host = await mountRunnerHost({ title: "keybindings", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -836,7 +837,8 @@ describe("helpItems", () => { test("lists every catalog row then Close help", () => { const items = helpItems(); expect(items).toHaveLength(SHELL_SHORTCUTS.length + 1); - expect(items[0]).toBe(`${SHELL_SHORTCUTS[0]!.keys} — ${SHELL_SHORTCUTS[0]!.description}`); + const first = defined(SHELL_SHORTCUTS[0]); + expect(items[0]).toBe(`${first.keys} — ${first.description}`); expect(items[items.length - 1]).toBe("Close help"); }); }); diff --git a/src/tui/landing.test.ts b/src/tui/landing.test.ts index 5f56dda2b..f9c2d7d32 100644 --- a/src/tui/landing.test.ts +++ b/src/tui/landing.test.ts @@ -6,6 +6,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { CapturedSpan } from "@opentui/core"; import { rgbToHex } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { makePermissionItems, withTestRenderer, type Harness } from "./harness"; import { appendStreamRow, @@ -219,8 +220,8 @@ describe("landing screen", () => { const row = painted.find((line) => line.includes(hint.rest)); expect(row).toBeDefined(); expect(row).toContain(hint.key); - expect(row!.indexOf(hint.key)).toBeGreaterThan(0); - descriptionColumns.add(row!.indexOf(hint.rest)); + expect(defined(row).indexOf(hint.key)).toBeGreaterThan(0); + descriptionColumns.add(defined(row).indexOf(hint.rest)); } expect(descriptionColumns.size).toBe(1); // The version is chrome, not part of the hero: it never shares a row @@ -235,7 +236,7 @@ describe("landing screen", () => { // Bottom-right: on the terminal's last content row, hugging the right // edge rather than sitting under the hints. expect(versionRow).toBeGreaterThanOrEqual(SIZE.height - 2); - const versionCol = painted[versionRow]!.lastIndexOf(LANDING_VERSION); + const versionCol = defined(painted[versionRow]).lastIndexOf(LANDING_VERSION); expect(versionCol + LANDING_VERSION.length).toBeGreaterThan(SIZE.width - 4); const noticeRow = painted.findIndex((row) => row.includes("telemetry")); expect(noticeRow).toBeGreaterThan(bottom); @@ -432,13 +433,12 @@ describe("landing screen", () => { }); try { await settle(h); - const first = LANDING_SUGGESTIONS[0]; - expect(first).toBeDefined(); - expect(applyLandingSuggestion(shell, first!.key)).toBe(true); - expect(shell.prompt.value).toBe(first!.prompt); + const first = defined(LANDING_SUGGESTIONS[0]); + expect(applyLandingSuggestion(shell, first.key)).toBe(true); + expect(shell.prompt.value).toBe(first.prompt); // Already typed: the key is a character, not a shortcut. - expect(applyLandingSuggestion(shell, first!.key)).toBe(false); + expect(applyLandingSuggestion(shell, first.key)).toBe(false); } finally { shell.dispose(); } @@ -454,7 +454,7 @@ describe("landing screen", () => { }); try { await settle(h); - const first = LANDING_SUGGESTIONS[0]!; + const first = defined(LANDING_SUGGESTIONS[0]); expect(h.captureCharFrame()).toContain(first.label); shell.prompt.value = "wri"; @@ -500,7 +500,7 @@ describe("landing screen", () => { // box sits one row above the terminal's last line — the optical // bottom pad (`BOTTOM_MARGIN_ROWS`) keeps it off the frame edge. expect(ruleRow).toBe(SIZE.height - 2); - const row = painted[ruleRow]!; + const row = defined(painted[ruleRow]); // Left end of the rule, inside the shell gutter, costing no row. expect(row.startsWith(" ╰─ ")).toBe(true); expect(row.trimEnd().endsWith("╯")).toBe(true); @@ -545,7 +545,7 @@ describe("landing screen", () => { try { await settle(h); const before = rows(h); - const anchors = ["message", "telemetry", LANDING_SUGGESTIONS[0]!.label]; + const anchors = ["message", "telemetry", defined(LANDING_SUGGESTIONS[0]).label]; const was = anchors.map((text) => before.findIndex((row) => row.includes(text))); expect(was.every((index) => index > 0)).toBe(true); // The anchors are listed top to bottom, so their positions climb @@ -651,7 +651,7 @@ describe("landing screen", () => { expect(field).toBeGreaterThan(0); expect(field).toBeLessThan(size.height); expect(markRows(h).length).toBeLessThan(field); - expect(h.captureCharFrame()).toContain(LANDING_HINTS[0]!.rest); + expect(h.captureCharFrame()).toContain(defined(LANDING_HINTS[0]).rest); } finally { shell.dispose(); } @@ -939,9 +939,8 @@ describe("landing screen", () => { // the combination of the task row and the version row. const promptRow = painted.findIndex((row) => row.includes("message")); expect(promptRow).toBeGreaterThan(0); - const box = shell.layout.regions.prompt; - expect(box).toBeDefined(); - expect(box!.y + box!.height).toBeLessThanOrEqual(size.height); + const box = defined(shell.layout.regions.prompt); + expect(box.y + box.height).toBeLessThanOrEqual(size.height); } finally { shell.dispose(); } diff --git a/src/tui/list-modal.test.ts b/src/tui/list-modal.test.ts index 1800cf1f4..68f65f079 100644 --- a/src/tui/list-modal.test.ts +++ b/src/tui/list-modal.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { createHarness, type Harness } from "./harness.js"; import { runListModal, type ListModalConfig } from "./list-modal.js"; @@ -21,7 +22,7 @@ async function mountModal(overrides: Partial = {}): Promise<{ { id: "s-1", label: "First session" }, { id: "s-2", label: "Second session" }, ], - createRenderer: async () => harness!.renderer, + createRenderer: async () => defined(harness, "harness").renderer, ...overrides, }); await harness.renderOnce(); diff --git a/src/tui/list-modal.ts b/src/tui/list-modal.ts index 31fbbfa17..477c08e3e 100644 --- a/src/tui/list-modal.ts +++ b/src/tui/list-modal.ts @@ -60,7 +60,7 @@ export async function runListModal(config: ListModalConfig): Promise void = () => {}; + let resolveChoice: (value: string | null) => void = () => undefined; const choice = new Promise((resolve) => { resolveChoice = resolve; }); diff --git a/src/tui/live-session-port.test.ts b/src/tui/live-session-port.test.ts index 25583d23a..46a6e1539 100644 --- a/src/tui/live-session-port.test.ts +++ b/src/tui/live-session-port.test.ts @@ -107,8 +107,8 @@ describe("attachment passthrough", () => { const seen: (readonly PendingImageAttachment[] | undefined)[] = []; const port = createLiveSessionPort({ send: (_text, attachments) => seen.push(attachments), - interrupt: () => {}, - deliver: () => {}, + interrupt: () => undefined, + deliver: () => undefined, }); port.sendImmediate("look", [image]); expect(seen).toEqual([[image]]); @@ -117,8 +117,8 @@ describe("attachment passthrough", () => { test("a queued item delivers its attachments at the boundary", () => { const seen: (readonly PendingImageAttachment[] | undefined)[] = []; const port = createLiveSessionPort({ - send: () => {}, - interrupt: () => {}, + send: () => undefined, + interrupt: () => undefined, deliver: (_text, _kind, attachments) => seen.push(attachments), }); port.deliver({ ...item("later", "queue"), attachments: [image] }); diff --git a/src/tui/markdown-parser.test.ts b/src/tui/markdown-parser.test.ts index eb93ee463..0caf0b33e 100644 --- a/src/tui/markdown-parser.test.ts +++ b/src/tui/markdown-parser.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { createMemoizedParseMarkdown, parseMarkdown, @@ -153,7 +154,7 @@ describe("block elements", () => { const steps = [base, `${base}\``, `${base}\`\``, `${base}\`\`\``]; const lineCounts = steps.map((content) => parseMarkdown(content).length); for (let i = 1; i < lineCounts.length; i++) { - expect(lineCounts[i]).toBeGreaterThanOrEqual(lineCounts[i - 1]!); + expect(lineCounts[i]).toBeGreaterThanOrEqual(defined(lineCounts[i - 1])); } }); diff --git a/src/tui/markdown-parser.ts b/src/tui/markdown-parser.ts index 08fcdb090..95b67904e 100644 --- a/src/tui/markdown-parser.ts +++ b/src/tui/markdown-parser.ts @@ -126,7 +126,9 @@ function parseSegments(text: string): StyledSegment[] { // A marker character that did not start a token (e.g. a lone `[`): emit it // as plain text and move on. - segments.push({ text: remaining[0]! }); + const ch = remaining[0]; + if (ch == null) break; + segments.push({ text: ch }); remaining = remaining.slice(1); offset += 1; } @@ -149,7 +151,9 @@ function parseLine(line: string): StyledSegment[] { // Headings h1–h6. The marker is stripped; inline markdown still applies. const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { - const level = headingMatch[1]!.length; + const hashes = headingMatch[1]; + if (hashes == null) throw new Error("heading marker missing"); + const level = hashes.length; return applyFlag(parseSegments(headingMatch[2] || ""), { heading: level }); } @@ -219,20 +223,25 @@ function fencedFoot(): StyledSegment[] { // unclosed while it streams; in that case a nascent closing fence is dropped so // the trailing block re-highlights cleanly rather than flickering. function parseFencedBlock(input: string[], start: number, width: number): FencedBlock { - const language = input[start]!.match(/^\s*(?:```+|~~~+)\s*([^\s`]*)/)?.[1] || undefined; + const openerLine = input[start]; + if (openerLine == null) throw new Error("fence opener missing"); + const language = openerLine.match(/^\s*(?:```+|~~~+)\s*([^\s`]*)/)?.[1] || undefined; const body: string[] = []; let i = start + 1; let closed = false; for (; i < input.length; i++) { - if (FENCE_CLOSE_RE.test(input[i]!)) { + const line = input[i]; + if (line == null) throw new Error("fence line missing"); + if (FENCE_CLOSE_RE.test(line)) { closed = true; break; } - body.push(input[i]!); + body.push(line); } const consumed = closed ? i - start + 1 : i - start; - if (!closed && body.length > 0 && PARTIAL_FENCE_RE.test(body[body.length - 1]!)) { + const last = body[body.length - 1]; + if (!closed && last != null && PARTIAL_FENCE_RE.test(last)) { body.pop(); } @@ -249,7 +258,9 @@ function parseIndentedCodeBlock(input: string[], start: number, width: number): const body: string[] = []; let i = start; for (; i < input.length; i++) { - const match = input[i]!.match(INDENTED_CODE_RE); + const line = input[i]; + if (line == null) throw new Error("indented code line missing"); + const match = line.match(INDENTED_CODE_RE); if (!match) break; body.push(match[1] ?? ""); } @@ -310,7 +321,8 @@ export function parseMarkdown(text: string, width = Infinity): StyledSegment[][] const input = text.split("\n"); for (let i = 0; i < input.length; i++) { - const line = input[i]!; + const line = input[i]; + if (line == null) throw new Error("markdown line missing"); // Fenced code block (``` or ~~~). The whole block is collected so its body // can be syntax-highlighted by the fence's language token; the delimiter @@ -480,7 +492,9 @@ function fitColumnWidths(naturalWidths: number[], targetContent: number): number } } if (widest < 0) break; - widths[widest]!--; + const width = widths[widest]; + if (width == null) break; + widths[widest] = width - 1; overflow--; } @@ -668,17 +682,24 @@ function fencedLineMask(lines: readonly string[]): boolean[] { const inside = new Array(lines.length).fill(false); let opener: { char: string; length: number } | null = null; for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (line == null) throw new Error("fence mask line missing"); if (opener === null) { - const match = lines[i]!.match(COMMONMARK_FENCE_OPEN_RE); + const match = line.match(COMMONMARK_FENCE_OPEN_RE); if (match) { inside[i] = true; - opener = { char: match[1]![0]!, length: match[1]!.length }; + const run = match[1]; + const char = run?.[0]; + if (run == null || char == null) throw new Error("fence opener capture missing"); + opener = { char, length: run.length }; } continue; } inside[i] = true; - const close = lines[i]!.match(COMMONMARK_FENCE_CLOSE_RE); - if (close && close[1]![0] === opener.char && close[1]!.length >= opener.length) { + const close = line.match(COMMONMARK_FENCE_CLOSE_RE); + const closeRun = close?.[1]; + const closeChar = closeRun?.[0]; + if (closeRun != null && closeChar === opener.char && closeRun.length >= opener.length) { opener = null; } } @@ -712,7 +733,9 @@ export function splitAtSettledHeading(text: string): MarkdownSplit | null { const insideFence = fencedLineMask(lines); let boundary = -1; for (let i = 0; i < lines.length; i += 1) { - if (!insideFence[i] && ATX_HEADING_LINE_RE.test(lines[i]!)) boundary = i; + const line = lines[i]; + if (line == null) continue; + if (!insideFence[i] && ATX_HEADING_LINE_RE.test(line)) boundary = i; } // No heading, or the last one is still the open tail: nothing to freeze. if (boundary === -1 || boundary >= lines.length - 1) return null; diff --git a/src/tui/markdown-rows.test.ts b/src/tui/markdown-rows.test.ts index eba32e726..e592e0245 100644 --- a/src/tui/markdown-rows.test.ts +++ b/src/tui/markdown-rows.test.ts @@ -5,6 +5,7 @@ import { describe, expect, test } from "bun:test"; import { MarkdownRenderable, BoxRenderable, type CapturedSpan } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { withTestRenderer, type Harness } from "./harness"; import { appendStreamRow, createStreamRowRenderable, replaceStreamRowAt } from "./shell/chrome"; import { createAppShell } from "./shell/index"; @@ -331,9 +332,9 @@ describe("markdown transcript rows", () => { await h.renderOnce(); const span = headingSpan(h); expect(span).not.toBeNull(); - expect(span!.text).toBe(baseline!.text); - expect(span!.fg).toEqual(baseline!.fg); - expect(span!.attributes).toBe(baseline!.attributes); + expect(defined(span).text).toBe(defined(baseline).text); + expect(defined(span).fg).toEqual(defined(baseline).fg); + expect(defined(span).attributes).toBe(defined(baseline).attributes); } }, WIDE); }); @@ -367,9 +368,9 @@ describe("markdown transcript rows", () => { const split = splitAtSettledHeading(text); expect(split).not.toBeNull(); // The fence opens and closes on the same side of the split. - expect(split!.frozen).toBe("### Title"); - expect(split!.live).toContain("```bash"); - expect(split!.live).toContain("```\n"); + expect(defined(split).frozen).toBe("### Title"); + expect(defined(split).live).toContain("```bash"); + expect(defined(split).live).toContain("```\n"); }); test("a fence opened before a heading keeps the heading out of the boundary search until it closes", () => { @@ -384,9 +385,9 @@ describe("markdown transcript rows", () => { ].join("\n"); const split = splitAtSettledHeading(text); expect(split).not.toBeNull(); - expect(split!.frozen).toContain("### Real Title"); - expect(split!.frozen).not.toContain("body text"); - expect(split!.live).toBe("body text"); + expect(defined(split).frozen).toContain("### Real Title"); + expect(defined(split).frozen).not.toContain("body text"); + expect(defined(split).live).toBe("body text"); }); test("a fenced `#` comment renders inside a matched fence, not split across two renderers", async () => { @@ -434,10 +435,10 @@ describe("markdown transcript rows", () => { ].join("\n"); const split = splitAtSettledHeading(text); expect(split).not.toBeNull(); - expect(split!.frozen).toContain("### Title"); - expect(split!.frozen).toContain("```stillcode"); - expect(split!.frozen).toContain("# should still be inside fence per CommonMark"); - expect(split!.live).toBe("body"); + expect(defined(split).frozen).toContain("### Title"); + expect(defined(split).frozen).toContain("```stillcode"); + expect(defined(split).frozen).toContain("# should still be inside fence per CommonMark"); + expect(defined(split).live).toBe("body"); }); test("adversarial fence pairings: length and character must both match, indentation is bounded", () => { @@ -450,37 +451,45 @@ describe("markdown transcript rows", () => { ).toBeNull(); // The same shape, properly closed by a run of 4+: now it is a heading. expect( - splitAtSettledHeading( - [ - "````", - "# not a heading", - "```", - "### not a heading either", - "````", - "", - "### Title", - "", - "body", - ].join("\n"), - )!.frozen, + defined( + splitAtSettledHeading( + [ + "````", + "# not a heading", + "```", + "### not a heading either", + "````", + "", + "### Title", + "", + "body", + ].join("\n"), + ), + ).frozen, ).toContain("### Title"); // Three backticks are closed by four (a longer run of the same char). expect( - splitAtSettledHeading( - ["```", "# not a heading", "````", "", "### Title", "", "body"].join("\n"), - )!.frozen, + defined( + splitAtSettledHeading( + ["```", "# not a heading", "````", "", "### Title", "", "body"].join("\n"), + ), + ).frozen, ).toContain("### Title"); // A tilde run never closes a backtick fence, or vice versa. expect( - splitAtSettledHeading( - ["```", "~~~", "# not a heading", "```", "### Title", "", "body"].join("\n"), - )!.frozen, + defined( + splitAtSettledHeading( + ["```", "~~~", "# not a heading", "```", "### Title", "", "body"].join("\n"), + ), + ).frozen, ).toContain("### Title"); // Up to 3 spaces of indent still opens/closes a fence. expect( - splitAtSettledHeading( - [" ```", "# not a heading", " ```", "### Title", "", "body"].join("\n"), - )!.frozen, + defined( + splitAtSettledHeading( + [" ```", "# not a heading", " ```", "### Title", "", "body"].join("\n"), + ), + ).frozen, ).toContain("### Title"); // 4 spaces is indented code, not a fence — the `#` line is still inside // it as indented code, never a heading boundary on its own. @@ -500,7 +509,7 @@ describe("markdown transcript rows", () => { test("an indented heading (CommonMark allows up to 3 leading spaces) still closes the split", () => { const split = splitAtSettledHeading([" ### Title", "", "body"].join("\n")); expect(split).not.toBeNull(); - expect(split!.frozen).toBe(" ### Title"); - expect(split!.live).toBe("body"); + expect(defined(split).frozen).toBe(" ### Title"); + expect(defined(split).live).toBe("body"); }); }); diff --git a/src/tui/mcp-copy-failure.test.ts b/src/tui/mcp-copy-failure.test.ts index 015d3c6a4..af45ae63e 100644 --- a/src/tui/mcp-copy-failure.test.ts +++ b/src/tui/mcp-copy-failure.test.ts @@ -38,8 +38,8 @@ describe("mcp auth copy failure", () => { const clip = { writeText: () => Promise.reject(new Error("both legs failed")) }; (shell as unknown as { clipboard: typeof clip }).clipboard = clip; openCommandSurface(shell, "mcp", { - notify: () => {}, - mcp: { list: () => entries, openAuthURL: () => {} }, + notify: () => undefined, + mcp: { list: () => entries, openAuthURL: () => undefined }, }); moveOverlaySelection(shell, 0); acceptOverlaySelection(shell); @@ -57,8 +57,8 @@ describe("mcp auth copy failure", () => { const clip = { writeText: () => Promise.resolve() }; (shell as unknown as { clipboard: typeof clip }).clipboard = clip; openCommandSurface(shell, "mcp", { - notify: () => {}, - mcp: { list: () => entries, openAuthURL: () => {} }, + notify: () => undefined, + mcp: { list: () => entries, openAuthURL: () => undefined }, }); moveOverlaySelection(shell, 0); acceptOverlaySelection(shell); diff --git a/src/tui/mcp-view.ts b/src/tui/mcp-view.ts index 7db4c7f8b..8646e48ed 100644 --- a/src/tui/mcp-view.ts +++ b/src/tui/mcp-view.ts @@ -271,7 +271,9 @@ const DETAIL_TEXT_MAX = 72; const TOOL_SEARCH_TOOL = "tool_search"; function titleCase(word: string): string { - return word.length === 0 ? word : `${word[0]!.toUpperCase()}${word.slice(1)}`; + const first = word[0]; + if (first == null) return word; + return `${first.toUpperCase()}${word.slice(1)}`; } function singular(noun: string): string { diff --git a/src/tui/mention-popup.test.ts b/src/tui/mention-popup.test.ts index 31e786161..212c67108 100644 --- a/src/tui/mention-popup.test.ts +++ b/src/tui/mention-popup.test.ts @@ -8,6 +8,7 @@ import { describe, expect, test } from "bun:test"; import type { KeyEvent } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { wireGates } from "./gate-wire"; import { withTestRenderer } from "./harness"; import { createAppShell } from "./shell/index"; @@ -106,7 +107,7 @@ function hangableSource(): { }; } -const ROOT = TREE[""]!; +const ROOT = defined(TREE[""]); describe("@ popup narrows as you type", () => { test("printable keys filter the list and land in the prompt", async () => { @@ -313,7 +314,7 @@ describe("@ popup narrows as you type", () => { subject: "bun test", scopes: [], }, - resolve: () => {}, + resolve: () => undefined, }); expect(shell.overlayKind).toBe("permissions"); diff --git a/src/tui/onboarding.test.ts b/src/tui/onboarding.test.ts index 4ddc86224..3857aaa2d 100644 --- a/src/tui/onboarding.test.ts +++ b/src/tui/onboarding.test.ts @@ -9,7 +9,7 @@ import type { WelcomeConfig } from "./welcome.js"; import { withMockedModule } from "../../tests/helpers/mock-module.js"; let testHome = ""; -let setup: (config: ProviderSetupConfig) => Promise = async () => {}; +let setup: (config: ProviderSetupConfig) => Promise = async () => undefined; let welcome: (config: WelcomeConfig) => Promise = async () => true; let tuiConfig: Config | undefined; const callOrder: string[] = []; @@ -91,7 +91,7 @@ async function writeXAIAuthProfile(home: string, profile: string): Promise } afterEach(() => { - setup = async () => {}; + setup = async () => undefined; welcome = async () => true; tuiConfig = undefined; callOrder.length = 0; @@ -116,7 +116,7 @@ describe("runOnboarding welcome gate", () => { model: "test-model", oauthProfile: "", }, - () => {}, + () => undefined, { skipValidation: true }, ); }; @@ -152,7 +152,7 @@ describe("runOnboarding welcome gate", () => { model: "test-model", oauthProfile: "", }, - () => {}, + () => undefined, { skipValidation: true }, ); }; @@ -208,7 +208,7 @@ describe("runOnboarding settings source", () => { model: "grok-4", oauthProfile: "work", }, - () => {}, + () => undefined, { skipValidation: true, oauth: { @@ -259,7 +259,7 @@ describe("runOnboarding settings source", () => { model: "test-model", oauthProfile: "", }, - () => {}, + () => undefined, { skipValidation: true }, ); }; @@ -304,7 +304,7 @@ describe("runOnboarding settings source", () => { model: "isolated-model", oauthProfile: "", }, - () => {}, + () => undefined, { skipValidation: true }, ); }; @@ -337,7 +337,7 @@ describe("runOnboarding settings source", () => { model: "isolated-model", oauthProfile: "", }, - () => {}, + () => undefined, { skipValidation: true }, ); }; diff --git a/src/tui/overlay-overflow.test.ts b/src/tui/overlay-overflow.test.ts index ecf164a99..476125db1 100644 --- a/src/tui/overlay-overflow.test.ts +++ b/src/tui/overlay-overflow.test.ts @@ -8,6 +8,7 @@ import { EventEmitter } from "node:events"; import { describe, expect, test } from "bun:test"; import type { PermissionRequest } from "../permission/types.js"; +import { defined } from "../../tests/helpers/defined.js"; import { withTestRenderer } from "./harness.js"; import { OVERLAY_MAX_FRACTION } from "./geometry/index.js"; import { appendStreamRow } from "./shell/chrome.js"; @@ -71,7 +72,7 @@ describe("approval overlay overflow (short terminal)", () => { }); expect(shell.overlayKind).toBe("permissions"); expect(shell.overlayList).not.toBeNull(); - const list = shell.overlayList!; + const list = defined(shell.overlayList, "overlayList"); // Host is fraction-capped: the window holds fewer items than exist. expect(list.height).toBeLessThan(items.length); expect(list.height).toBeGreaterThanOrEqual(1); @@ -85,16 +86,16 @@ describe("approval overlay overflow (short terminal)", () => { for (let i = 0; i < list.height + 3; i++) { moveOverlaySelection(shell, 1); } - expect(shell.overlayList!.activeIndex).toBe(list.height + 3); - expect(shell.overlayList!.offset).toBeGreaterThan(startOffset); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBe(list.height + 3); + expect(defined(shell.overlayList, "overlayList").offset).toBeGreaterThan(startOffset); activeVisible(shell); // Last choice is still reachable and accept closes the overlay. const last = items.length - 1; - while (shell.overlayList!.activeIndex < last) { + while (defined(shell.overlayList, "overlayList").activeIndex < last) { moveOverlaySelection(shell, 1); } - expect(shell.overlayList!.activeIndex).toBe(last); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBe(last); activeVisible(shell); acceptOverlaySelection(shell); expect(shell.overlayList).toBeNull(); @@ -117,14 +118,14 @@ describe("approval overlay overflow (short terminal)", () => { items, body: "run_shell\nRun shell command\nbun test", }); - const list = shell.overlayList!; + const list = defined(shell.overlayList, "overlayList"); expect(list.count).toBe(2); // On a short terminal the host fraction can leave only one list row. // Both choices must still be reachable and accept must close. expect(list.height).toBeGreaterThanOrEqual(1); expect(list.offset).toBe(0); moveOverlaySelection(shell, 1); - expect(shell.overlayList!.activeIndex).toBe(1); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBe(1); activeVisible(shell); acceptOverlaySelection(shell); expect(shell.overlayList).toBeNull(); @@ -147,7 +148,7 @@ describe("approval overlay overflow (short terminal)", () => { choices: manyChoices, }); expect(shell.overlayKind).toBe("operator"); - const list = shell.overlayList!; + const list = defined(shell.overlayList, "overlayList"); expect(list.height).toBeLessThan(manyChoices.length); expect(list.offset).toBe(0); @@ -155,8 +156,8 @@ describe("approval overlay overflow (short terminal)", () => { moveOverlaySelection(shell, 1); activeVisible(shell); } - expect(shell.overlayList!.activeIndex).toBe(manyChoices.length - 1); - expect(shell.overlayList!.offset).toBeGreaterThan(0); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBe(manyChoices.length - 1); + expect(defined(shell.overlayList, "overlayList").offset).toBeGreaterThan(0); // Keep active in the viewport window (offset/height contract), not a // frame substring — on short terminals the tall body can own the host // paint while the list still scrolls in state. @@ -183,14 +184,14 @@ describe("approval overlay overflow (short terminal)", () => { primeSession(shell); const items = makePermissionItems(40); openPermissionsOverlay(shell, { items, body: tallBody }); - const list = shell.overlayList!; + const list = defined(shell.overlayList, "overlayList"); // Even at 24 rows the fraction cap can force a window smaller than 40. if (list.height < items.length) { const start = list.offset; for (let i = 0; i < list.height + 2; i++) { moveOverlaySelection(shell, 1); } - expect(shell.overlayList!.offset).toBeGreaterThan(start); + expect(defined(shell.overlayList, "overlayList").offset).toBeGreaterThan(start); activeVisible(shell); } else { // Cap did not bind; every item is already visible without scroll. @@ -237,11 +238,11 @@ describe("gate-wire approval overflow on short terminal", () => { const choices = permissionChoicesFromRequest(request, "req-1"); expect(shell.overlayKind).toBe("permissions"); expect(shell.overlayItems).toEqual([...choices.items]); - const list = shell.overlayList!; + const list = defined(shell.overlayList, "overlayList"); expect(list.height).toBeLessThan(choices.items.length); const last = choices.items.length - 1; - while (shell.overlayList!.activeIndex < last) { + while (defined(shell.overlayList, "overlayList").activeIndex < last) { moveOverlaySelection(shell, 1); } activeVisible(shell); @@ -281,11 +282,11 @@ describe("gate-wire approval overflow on short terminal", () => { const choices = operatorChoicesFromOptions(options, "ask-1"); expect(shell.overlayKind).toBe("operator"); expect(shell.overlayItems).toEqual([...choices.items]); - const list = shell.overlayList!; + const list = defined(shell.overlayList, "overlayList"); expect(list.height).toBeLessThan(options.length); const target = options.length - 1; - while (shell.overlayList!.activeIndex < target) { + while (defined(shell.overlayList, "overlayList").activeIndex < target) { moveOverlaySelection(shell, 1); } activeVisible(shell); @@ -314,7 +315,7 @@ describe("gate-wire approval overflow on short terminal", () => { try { primeSession(shell); const dispose = wireGates(emitter, shell); - emitter.emit("permission.gate", { id: "req-1", request, resolve: () => {} }); + emitter.emit("permission.gate", { id: "req-1", request, resolve: () => undefined }); const body = permissionBodyFromRequest(request, { hint: true }); // The raw body still carries the collapsed-command hint — only what diff --git a/src/tui/overlays.test.ts b/src/tui/overlays.test.ts index 503414123..2368df34b 100644 --- a/src/tui/overlays.test.ts +++ b/src/tui/overlays.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, test } from "bun:test"; import { rgbToHex, type KeyEvent } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { IDLE_TRANSCRIPT_FLOOR, OVERLAY_TRANSCRIPT_FLOOR } from "./geometry/index"; import { focusOwner, scrollLease } from "./focus/index"; import { @@ -83,7 +84,7 @@ describe("permissions overlay", () => { expect(shell.overlayKind).toBe("permissions"); expect(shell.overlayList).not.toBeNull(); expect(shell.overlayItems.length).toBe(30); - expect(shell.overlayList!.activeIndex).toBe(0); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBe(0); expect(focusOwner(shell.focus)).toBe("overlay"); expect(scrollLease(shell.focus)).toBe("overlay"); expect(shell.layout.overlayMode).toBe("inset"); @@ -100,18 +101,18 @@ describe("permissions overlay", () => { expect(frame).toContain("Esc cancel · Enter choose · /yolo skip prompts"); // Navigate deep enough that window must scroll (keep-active-visible). - const listH = shell.overlayList!.height; + const listH = defined(shell.overlayList, "overlayList").height; for (let i = 0; i < listH + 5; i++) { moveOverlaySelection(shell, 1); } - expect(shell.overlayList!.activeIndex).toBe(listH + 5); - const slice = shell.overlayList!.visibleRange(); - expect(shell.overlayList!.activeIndex).toBeGreaterThanOrEqual(slice.start); - expect(shell.overlayList!.activeIndex).toBeLessThan(slice.end); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBe(listH + 5); + const slice = defined(shell.overlayList, "overlayList").visibleRange(); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBeGreaterThanOrEqual(slice.start); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBeLessThan(slice.end); await h.renderOnce(); frame = h.captureCharFrame(); - const activeLabel = shell.overlayItems[shell.overlayList!.activeIndex] ?? ""; + const activeLabel = shell.overlayItems[defined(shell.overlayList, "overlayList").activeIndex] ?? ""; expect(frame).toContain(activeLabel.slice(0, 20)); h.pressKey("Escape"); @@ -185,12 +186,12 @@ describe("permissions overlay", () => { }); try { openPermissionsOverlay(shell, { items: makePermissionItems(30) }); - const before = shell.overlayList!.activeIndex; + const before = defined(shell.overlayList, "overlayList").activeIndex; pageOverlaySelection(shell, 1); - expect(shell.overlayList!.activeIndex).toBeGreaterThan(before); - const slice = shell.overlayList!.visibleRange(); - expect(shell.overlayList!.activeIndex).toBeGreaterThanOrEqual(slice.start); - expect(shell.overlayList!.activeIndex).toBeLessThan(slice.end); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBeGreaterThan(before); + const slice = defined(shell.overlayList, "overlayList").visibleRange(); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBeGreaterThanOrEqual(slice.start); + expect(defined(shell.overlayList, "overlayList").activeIndex).toBeLessThan(slice.end); } finally { shell.dispose(); } diff --git a/src/tui/palette-paint.test.ts b/src/tui/palette-paint.test.ts index 21dbcec22..9cb274f0f 100644 --- a/src/tui/palette-paint.test.ts +++ b/src/tui/palette-paint.test.ts @@ -6,6 +6,7 @@ import { describe, expect, test } from "bun:test"; import type { KeyEvent } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { withTestRenderer } from "./harness"; import type { PaletteCommand } from "./command-catalog"; import { createAppShell } from "./shell/index"; @@ -222,8 +223,7 @@ function zoneAfterList( function expectNameOnlyRows(lines: readonly string[], labels: readonly string[]): void { for (const label of labels) { const row = lines.find((r) => r.includes(label)); - expect(row).toBeDefined(); - expect(row!.trim()).toBe(label); + expect(defined(row, "row").trim()).toBe(label); } } @@ -238,8 +238,7 @@ function expectDescriptionUnderListRule( expect(row).not.toContain(description); } const zone = zoneAfterList(lines, labels); - expect(zone).toBeDefined(); - expect(zone!.some((r) => r.includes(description))).toBe(true); + expect(defined(zone, "zone").some((r) => r.includes(description))).toBe(true); } describe("command list description zone", () => { @@ -313,8 +312,7 @@ describe("command list description zone", () => { const blank = stripFrameLines(h.captureCharFrame()); expectNameOnlyRows(blank, labels); const zone = zoneAfterList(blank, labels); - expect(zone).toBeDefined(); - expect(zone!.every((r) => r.trim() === "")).toBe(true); + expect(defined(zone, "zone").every((r) => r.trim() === "")).toBe(true); expect(blank.join("\n")).not.toContain(HELP_DESC); expect(shell.layout.heights.overlay_host).toBe(reserved); }, @@ -367,15 +365,21 @@ describe("command list selection colour", () => { const groundLine = frame.lines.find((line) => line.spans.some((s) => s.text.includes("/model")), ); - expect(activeLine).toBeDefined(); - expect(groundLine).toBeDefined(); - const activeBg = activeLine!.spans[0]!.bg; - const groundBg = groundLine!.spans[0]!.bg; + const active = defined(activeLine, "activeLine"); + const ground = defined(groundLine, "groundLine"); + const activeBg = defined(active.spans[0], "activeLine.spans[0]").bg; + const groundBg = defined(ground.spans[0], "groundLine.spans[0]").bg; // Same background either way — selection reads through text colour // (fg), not a filled band behind the row. expect(activeBg).toEqual(groundBg); - const activeFg = activeLine!.spans.find((s) => s.text.includes("/help"))!.fg; - const groundFg = groundLine!.spans.find((s) => s.text.includes("/model"))!.fg; + const activeFg = defined( + active.spans.find((s) => s.text.includes("/help")), + "help span", + ).fg; + const groundFg = defined( + ground.spans.find((s) => s.text.includes("/model")), + "model span", + ).fg; expect(activeFg).not.toEqual(groundFg); }, { width: 100, height: 32 }, diff --git a/src/tui/plugins-admin-backend.ts b/src/tui/plugins-admin-backend.ts index 5c76c3eb6..b83d20e28 100644 --- a/src/tui/plugins-admin-backend.ts +++ b/src/tui/plugins-admin-backend.ts @@ -381,7 +381,7 @@ export function createPluginsAdmin(args: { otherLivePluginPaths: state.modules.flatMap((m) => m.manifest?.id !== id && m.pluginPath !== undefined ? [m.pluginPath] : [], ), - expandMembers: (abs) => expandPluginPath(abs, { onSkip: () => {} }), + expandMembers: (abs) => expandPluginPath(abs, { onSkip: () => undefined }), revokePathPlugin: async (path) => { state.pathTrust = await revokePathPlugin(path); }, diff --git a/src/tui/product-host.test.ts b/src/tui/product-host.test.ts index 0d8664a8d..839369111 100644 --- a/src/tui/product-host.test.ts +++ b/src/tui/product-host.test.ts @@ -216,7 +216,7 @@ describe("mountProductHost", () => { id: "ask-1", question: "Proceed?", options: ["Cancel", "Continue"], - resolve: (_result: unknown) => {}, + resolve: (_result: unknown) => undefined, }); expect(host.shell.overlayKind).toBe("operator"); expect(host.shell.overlayItems).toEqual(["Cancel", "Continue"]); @@ -442,7 +442,7 @@ describe("flat type-to-filter model picker", () => { createRenderer: async () => harness.renderer, models: catalog, activeModelId: () => modelOptionId("xai/thegreataxios", "grok-4.5"), - onModelSelect: () => {}, + onModelSelect: () => undefined, }); try { host.openModels?.(); @@ -475,7 +475,7 @@ describe("flat type-to-filter model picker", () => { createRenderer: async () => harness.renderer, models: catalog, activeModelId: () => modelOptionId("codex/abk-labs", "gpt-5.5"), - onModelSelect: () => {}, + onModelSelect: () => undefined, }); try { host.openModels?.(); @@ -502,7 +502,7 @@ describe("flat type-to-filter model picker", () => { deliver: port.deliver, createRenderer: async () => harness.renderer, models: catalog, - onModelSelect: () => {}, + onModelSelect: () => undefined, }); try { host.openModels?.(); @@ -627,7 +627,7 @@ describe("flat type-to-filter model picker", () => { test("the model picker footer advertises Alt+D when onSetDefault is wired", async () => { const { harness, host } = await mountPicker({ - onSetDefault: () => {}, + onSetDefault: () => undefined, }); try { host.openModels?.(); @@ -657,7 +657,7 @@ describe("flat type-to-filter model picker", () => { const { harness, host } = await mountPicker({ // The hint requires the full wiring — choices AND the connect handler — // because that is exactly when the key actually works. - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -674,7 +674,7 @@ describe("flat type-to-filter model picker", () => { test("Alt+A opens the add-provider selector listing every provider kind and its account count", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [ { id: "codex", label: "Codex", hint: "ChatGPT subscription", accountCount: 2 }, { id: "openai", label: "OpenAI", hint: "", accountCount: 0 }, @@ -701,7 +701,7 @@ describe("flat type-to-filter model picker", () => { test("composed Option+A (å) opens add-provider and is not claimed by type-to-filter", async () => { // Terminals may deliver Option+A as å/Å without meta/option. const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -725,7 +725,7 @@ describe("flat type-to-filter model picker", () => { test("composed Option+A (Å) opens add-provider and is not claimed by type-to-filter", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -749,7 +749,7 @@ describe("flat type-to-filter model picker", () => { test("composed å through the key path opens add-provider", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -766,7 +766,7 @@ describe("flat type-to-filter model picker", () => { test("closed-prompt å stays in the prompt and does not open add-provider", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -783,7 +783,7 @@ describe("flat type-to-filter model picker", () => { test("other composed glyphs still type-to-filter in the model picker", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -810,7 +810,7 @@ describe("flat type-to-filter model picker", () => { // Terminals can report Option+A as sequence å while name stays ASCII a // and option/meta stay false (#482). const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -854,7 +854,7 @@ describe("flat type-to-filter model picker", () => { test("bare ASCII a still type-to-filters when add-provider is wired", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -877,7 +877,7 @@ describe("flat type-to-filter model picker", () => { test("ordinary letters still type-to-filter when add-provider is wired", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -924,7 +924,7 @@ describe("flat type-to-filter model picker", () => { test("Esc from the add-provider selector returns to the model list", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 1 }], }); try { @@ -947,7 +947,7 @@ describe("flat type-to-filter model picker", () => { test("Esc after openAddProvider from a closed prompt does not reopen the model list", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 1 }], }); try { @@ -969,7 +969,7 @@ describe("flat type-to-filter model picker", () => { test("typed /connect then Enter opens add-provider and Esc leaves overlay null", async () => { const queued: { open?: () => void } = {}; const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 1 }], commands: [ { @@ -1049,7 +1049,7 @@ describe("flat type-to-filter model picker", () => { test("openAddProvider opens the add-provider selector when choices are wired", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { @@ -1137,7 +1137,7 @@ describe("flat type-to-filter model picker", () => { test("setModels does not steal an open add-provider overlay", async () => { const { harness, host } = await mountPicker({ - onConnectProvider: () => {}, + onConnectProvider: () => undefined, addProviderChoices: () => [{ id: "codex", label: "Codex", hint: "", accountCount: 0 }], }); try { diff --git a/src/tui/prompt-border.test.ts b/src/tui/prompt-border.test.ts index 69b8528cc..61d0c0caa 100644 --- a/src/tui/prompt-border.test.ts +++ b/src/tui/prompt-border.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { BORDER, MCP_ATTENTION_LABEL, @@ -156,7 +157,7 @@ describe("composeRule", () => { const parts = composeRule({ width: 48, corners: TOP, - attention: attention!, + attention: defined(attention), label: "xai · grok", }); expect(ruleText(parts)).toContain("mcp ! · plugin !"); @@ -211,22 +212,25 @@ describe("composeCostContextMeter", () => { }); test("carries the percent and cost", () => { - const meter = composeCostContextMeter({ - contextPercentUsed: 68, - costLabel: "$0.42", - contextIsEstimate: false, - }); - expect(meter).not.toBeNull(); - expect(meter!.percentLabel).toBe("68%"); - expect(meter!.costLabel).toBe("$0.42"); + const meter = defined( + composeCostContextMeter({ + contextPercentUsed: 68, + costLabel: "$0.42", + contextIsEstimate: false, + }), + ); + expect(meter.percentLabel).toBe("68%"); + expect(meter.costLabel).toBe("$0.42"); }); test("drops the cost suffix when told to, keeping the percent", () => { - const meter = composeCostContextMeter({ - contextPercentUsed: 68, - costLabel: "$0.42", - contextIsEstimate: false, - })!; + const meter = defined( + composeCostContextMeter({ + contextPercentUsed: 68, + costLabel: "$0.42", + contextIsEstimate: false, + }), + ); expect(costContextText(meter, true)).toContain("$0.42"); expect(costContextText(meter, false)).not.toContain("$0.42"); expect(costContextText(meter, false)).toContain("68%"); @@ -234,7 +238,8 @@ describe("composeCostContextMeter", () => { test("bands from the percent: 60 quiet, 80 warning, 81 danger", () => { const bandAt = (percent: number) => - composeCostContextMeter({ contextPercentUsed: percent, contextIsEstimate: false })!.band; + defined(composeCostContextMeter({ contextPercentUsed: percent, contextIsEstimate: false })) + .band; expect(bandAt(0)).toBe("quiet"); expect(bandAt(60)).toBe("quiet"); expect(bandAt(61)).toBe("warning"); @@ -244,7 +249,9 @@ describe("composeCostContextMeter", () => { }); test("flags an estimated percent with a tilde", () => { - const meter = composeCostContextMeter({ contextPercentUsed: 68, contextIsEstimate: true })!; + const meter = defined( + composeCostContextMeter({ contextPercentUsed: 68, contextIsEstimate: true }), + ); expect(meter.percentLabel).toBe("~68%"); }); }); diff --git a/src/tui/prompt-box.test.ts b/src/tui/prompt-box.test.ts index 80a5f2d4f..b7af84bce 100644 --- a/src/tui/prompt-box.test.ts +++ b/src/tui/prompt-box.test.ts @@ -4,6 +4,7 @@ */ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { PROMPT_KEY_BINDINGS } from "./prompt-input"; import { withTestRenderer, type Harness } from "./harness"; import { PROMPT_BASE_ROWS, PROMPT_CAP_FRACTION, PROMPT_IDLE_ROWS } from "./geometry/index.js"; @@ -128,9 +129,8 @@ describe("prompt box height", () => { // the version badge reserves the terminal's last row. await withShell({ columns: 80, rows: 31 }, async (shell, h) => { await compose(shell, h, lines(5)); - const box = shell.layout.regions.prompt; - expect(box).toBeDefined(); - expect(box!.y + box!.height).toBe(30); + const box = defined(shell.layout.regions.prompt); + expect(box.y + box.height).toBe(30); }); }); }); diff --git a/src/tui/prompt-chrome.test.ts b/src/tui/prompt-chrome.test.ts index 0de8465ef..3c24e4e2d 100644 --- a/src/tui/prompt-chrome.test.ts +++ b/src/tui/prompt-chrome.test.ts @@ -50,7 +50,7 @@ describe("bare exit / quit at the prompt", () => { let exits = 0; setShellBridgeHooks(shell, { onSubmit: (text) => sent.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); setShellExitHandler(shell, () => { @@ -71,7 +71,7 @@ describe("bare exit / quit at the prompt", () => { let exits = 0; setShellBridgeHooks(shell, { onSubmit: (text) => sent.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); setShellExitHandler(shell, () => { @@ -89,7 +89,7 @@ describe("bare exit / quit at the prompt", () => { const sent: string[] = []; setShellBridgeHooks(shell, { onSubmit: (text) => sent.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); shell.prompt.value = "exit"; @@ -303,7 +303,7 @@ describe("no permanent hint strip", () => { schedule: (fn, ms) => { expect(ms).toBe(RUNTIME_FLASH_MS); lapse.push(fn); - return () => {}; + return () => undefined; }, }); expect(noticeText(shell)).toContain("copied 3 lines"); @@ -329,7 +329,7 @@ describe("no permanent hint strip", () => { flashSchedule: (fn, ms) => { expect(ms).toBe(RUNTIME_FLASH_MS); lapse.push(fn); - return () => {}; + return () => undefined; }, }); setStatusFlash(shell, "copied 3 lines", { ttlMs: RUNTIME_FLASH_MS }); diff --git a/src/tui/prompt-features.test.ts b/src/tui/prompt-features.test.ts index 01aa5a0a8..7f4b00048 100644 --- a/src/tui/prompt-features.test.ts +++ b/src/tui/prompt-features.test.ts @@ -101,7 +101,7 @@ describe("image attachments", () => { const flashSchedule: FlashSchedule = (fn, ms) => { expect(ms).toBe(RUNTIME_FLASH_MS); lapse.push(fn); - return () => {}; + return () => undefined; }; await withShell( @@ -145,7 +145,7 @@ describe("image attachments", () => { wireKeys: true, run: "idle", }); - let resolveRead: (r: { ok: true; attachment: PendingImageAttachment }) => void = () => {}; + let resolveRead: (r: { ok: true; attachment: PendingImageAttachment }) => void = () => undefined; setPromptImageSource( shell, () => @@ -183,7 +183,7 @@ describe("image attachments", () => { run: "idle", }); try { - let resolveAttached: () => void = () => {}; + let resolveAttached: () => void = () => undefined; const attached = new Promise((r) => { resolveAttached = r; }); @@ -252,7 +252,7 @@ describe("image attachments", () => { const seen: (readonly PendingImageAttachment[] | undefined)[] = []; setShellBridgeHooks(shell, { onSubmit: (_text, _kind, attachments) => seen.push(attachments), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); setPromptImageSource(shell, async () => ({ ok: true, attachment: CLIP })); @@ -270,7 +270,7 @@ describe("image attachments", () => { const texts: string[] = []; setShellBridgeHooks(shell, { onSubmit: (text) => texts.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); setPromptImageSource(shell, async () => ({ ok: true, attachment: CLIP })); @@ -285,7 +285,7 @@ describe("image attachments", () => { const submitted: string[] = []; setShellBridgeHooks(shell, { onSubmit: (text) => submitted.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); shell.prompt.value = " "; @@ -315,7 +315,7 @@ describe("text paste", () => { const submitted: string[] = []; setShellBridgeHooks(shell, { onSubmit: (text) => submitted.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); setPromptImageSource(shell, async () => ({ ok: true, attachment: CLIP })); @@ -385,7 +385,7 @@ describe("un-bracketed paste vs. deliberate Enter", () => { const submitted: string[] = []; setShellBridgeHooks(shell, { onSubmit: (text) => submitted.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); shell.prompt.focus(); @@ -426,7 +426,7 @@ describe("un-bracketed paste vs. deliberate Enter", () => { const submitted: string[] = []; setShellBridgeHooks(shell, { onSubmit: (text) => submitted.push(text), - onInterrupt: () => {}, + onInterrupt: () => undefined, exclusive: true, }); shell.prompt.focus(); @@ -488,8 +488,8 @@ describe("sent-message recall", () => { test("submitting records the message for later recall", async () => { await withShell(async (shell) => { setShellBridgeHooks(shell, { - onSubmit: () => {}, - onInterrupt: () => {}, + onSubmit: () => undefined, + onInterrupt: () => undefined, exclusive: true, }); shell.prompt.value = "remember me"; @@ -550,7 +550,7 @@ describe("@-mention suggestions", () => { run: "idle", }); try { - let resolveOpened: () => void = () => {}; + let resolveOpened: () => void = () => undefined; const opened = new Promise((r) => { resolveOpened = r; }); diff --git a/src/tui/prompt-kill-ring.test.ts b/src/tui/prompt-kill-ring.test.ts index bfd7d7c76..6c2ba9904 100644 --- a/src/tui/prompt-kill-ring.test.ts +++ b/src/tui/prompt-kill-ring.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { beginYank, breakKillSequence, @@ -14,21 +15,21 @@ describe("recordKill / beginYank", () => { const ring = recordKill(emptyKillRing, "world", "forward"); const yank = beginYank(ring, 5); expect(yank).not.toBeNull(); - expect(yank!.text).toBe("world"); + expect(defined(yank).text).toBe("world"); }); test("consecutive forward kills accumulate in order", () => { let ring = recordKill(emptyKillRing, "foo", "forward"); ring = recordKill(ring, "bar", "forward"); const yank = beginYank(ring, 0); - expect(yank!.text).toBe("foobar"); + expect(defined(yank).text).toBe("foobar"); }); test("consecutive backward kills prepend so original order survives", () => { let ring = recordKill(emptyKillRing, "bar", "backward"); ring = recordKill(ring, "foo", "backward"); const yank = beginYank(ring, 0); - expect(yank!.text).toBe("foobar"); + expect(defined(yank).text).toBe("foobar"); }); test("a non-kill breaks accumulation: a later kill starts a fresh entry", () => { @@ -49,12 +50,12 @@ describe("rotateYank", () => { test("rotates to the next-older entry after a yank", () => { let ring = recordKill(emptyKillRing, "second", "forward"); ring = recordKill(breakKillSequence(ring), "first", "forward"); - const yank = beginYank(ring, 0)!; + const yank = defined(beginYank(ring, 0)); expect(yank.text).toBe("first"); const rotated = rotateYank(yank.ring); expect(rotated).not.toBeNull(); - expect(rotated!.text).toBe("second"); - expect(rotated!.span).toEqual({ start: 0, end: 5 }); + expect(defined(rotated).text).toBe("second"); + expect(defined(rotated).span).toEqual({ start: 0, end: 5 }); }); test("returns null when the previous command was not a yank", () => { @@ -69,10 +70,10 @@ describe("rotateYank", () => { test("wraps back to the first entry after cycling through all of them", () => { let ring = recordKill(emptyKillRing, "b", "forward"); ring = recordKill(breakKillSequence(ring), "a", "forward"); - const yank = beginYank(ring, 0)!; - const once = rotateYank(yank.ring)!; + const yank = defined(beginYank(ring, 0)); + const once = defined(rotateYank(yank.ring)); expect(once.text).toBe("b"); - const twice = rotateYank(once.ring)!; + const twice = defined(rotateYank(once.ring)); expect(twice.text).toBe("a"); }); }); diff --git a/src/tui/prompt-kill-ring.ts b/src/tui/prompt-kill-ring.ts index a0f1c3de7..f73ba2d22 100644 --- a/src/tui/prompt-kill-ring.ts +++ b/src/tui/prompt-kill-ring.ts @@ -61,12 +61,12 @@ export function recordKill( const accumulating = (ring.lastAction === "kill-forward" || ring.lastAction === "kill-backward") && ring.entries.length > 0; - const entries = accumulating - ? [ - direction === "forward" ? ring.entries[0]! + text : text + ring.entries[0]!, - ...ring.entries.slice(1), - ] - : [text, ...ring.entries].slice(0, KILL_RING_MAX); + const head = ring.entries[0]; + if (accumulating && head == null) throw new Error("kill ring entry missing"); + const entries = + accumulating && head != null + ? [direction === "forward" ? head + text : text + head, ...ring.entries.slice(1)] + : [text, ...ring.entries].slice(0, KILL_RING_MAX); return { entries, yankIndex: 0, @@ -101,7 +101,8 @@ export function rotateYank( if (ring.lastAction !== "yank" || ring.lastYankSpan === null) return null; if (ring.entries.length === 0) return null; const nextIndex = (ring.yankIndex + 1) % ring.entries.length; - const text = ring.entries[nextIndex]!; + const text = ring.entries[nextIndex]; + if (text == null) return null; const span = ring.lastYankSpan; return { text, diff --git a/src/tui/prompt-slash-exit.test.ts b/src/tui/prompt-slash-exit.test.ts index 1e94f2680..e3476ed77 100644 --- a/src/tui/prompt-slash-exit.test.ts +++ b/src/tui/prompt-slash-exit.test.ts @@ -218,7 +218,7 @@ describe("Ctrl+C exit", () => { schedule: (fn, ms) => { expect(ms).toBe(CTRL_C_EXIT_WINDOW_MS); lapse.push(fn); - return () => {}; + return () => undefined; }, }); expect(shell.statusFlash).toBe("press ctrl+c again to exit"); @@ -237,7 +237,7 @@ describe("Ctrl+C exit", () => { handleCtrlC(shell, 0, { schedule: (fn) => { lapse.push(fn); - return () => {}; + return () => undefined; }, }); setStatusFlash(shell, "copied 3 lines", { diff --git a/src/tui/provider-connect.test.ts b/src/tui/provider-connect.test.ts index 4ba5f7f3d..be2fc90ac 100644 --- a/src/tui/provider-connect.test.ts +++ b/src/tui/provider-connect.test.ts @@ -3,6 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { defined } from "../../tests/helpers/defined.js"; import { createHarness, type Harness } from "./harness.js"; import { connectProviderInline } from "./provider/connect.js"; import { loadSettings } from "../config/settings.js"; @@ -29,7 +30,7 @@ describe("connectProviderInline", () => { settingsPath, localSettingsPath: join(dir, "local.json"), existing: null, - createRenderer: async () => harness!.renderer, + createRenderer: async () => defined(harness, "harness").renderer, }); await harness.renderOnce(); diff --git a/src/tui/provider-setup-submit.test.ts b/src/tui/provider-setup-submit.test.ts index 55e959fc5..aad7bb4b2 100644 --- a/src/tui/provider-setup-submit.test.ts +++ b/src/tui/provider-setup-submit.test.ts @@ -40,7 +40,7 @@ const { loadLocalSettings, loadSettings, localSettingsPath, resolveLocalSettings await import("../config/settings.js"); import type { OAuthResult, ProviderFormValues, SubmitPhase } from "./provider/types.js"; -const noopSetPhase = (_phase: SubmitPhase): void => {}; +const noopSetPhase = (_phase: SubmitPhase): void => undefined; const stagedCodexTokens = { access: "staged-access", refresh: "staged-refresh", @@ -48,7 +48,7 @@ const stagedCodexTokens = { accountId: "staged-account", }; -function stagedCodexOAuth(commit: () => Promise = async () => {}): OAuthResult { +function stagedCodexOAuth(commit: () => Promise = async () => undefined): OAuthResult { return { kind: "codex", providerName: "codex/work", diff --git a/src/tui/provider-setup.test.ts b/src/tui/provider-setup.test.ts index 7c8cb74ba..51b7078b4 100644 --- a/src/tui/provider-setup.test.ts +++ b/src/tui/provider-setup.test.ts @@ -3,6 +3,8 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { defined } from "../../tests/helpers/defined.js"; + import { OAuthProviderScopeError } from "../auth/oauth-scope-check.js"; import { OPENCODE_GO_MODEL_IDS } from "../../packages/opencode-go/src/index.js"; import { resetGoModelDiscoveryForTests } from "../provider/opencode-go-models.js"; @@ -70,7 +72,7 @@ function stagedLogin(profile: string): LoginCompletion { tokens: { access: "test-access", refresh: "test-refresh", expiresAt: 10_000 }, createdAt: 1, }, - commit: async () => {}, + commit: async () => undefined, }; } @@ -86,7 +88,7 @@ async function createHarness(opts: { width: number; height: number }): Promise { resetGoModelDiscoveryForTests(); - while (activeHarnesses.length > 0) activeHarnesses.pop()!.destroy(); + while (activeHarnesses.length > 0) defined(activeHarnesses.pop(), "harness").destroy(); }); beforeEach(() => { @@ -327,7 +329,7 @@ describe("provider setup pure helpers", () => { }); async function mountSetup( - onSubmit: ProviderSetupSubmit = async () => {}, + onSubmit: ProviderSetupSubmit = async () => undefined, showTelemetryNotice = false, existingProviderNames: readonly string[] = [], ): Promise<{ done: Promise; harness: Harness }> { @@ -430,7 +432,7 @@ async function mountLogin(opts: { }): Promise<{ done: Promise; harness: Harness }> { const harness = await createHarness({ width: 80, height: 30 }); const done = runProviderSetup({ - onSubmit: opts.onSubmit ?? (async () => {}), + onSubmit: opts.onSubmit ?? (async () => undefined), showTelemetryNotice: false, createRenderer: async () => harness.renderer, startLogin: opts.start, @@ -780,7 +782,7 @@ describe("runProviderSetup sign-in", () => { test("a subscription provider signs in in place and persists the selection", async () => { const seen: ProviderFormValues[] = []; const opts: SubmitOpts[] = []; - let complete: (result: LoginCompletion) => void = () => {}; + let complete: (result: LoginCompletion) => void = () => undefined; const { done, harness } = await mountLogin({ start: async ({ kind, profile }) => { expect(kind).toBe("codex"); @@ -790,7 +792,7 @@ describe("runProviderSetup sign-in", () => { completed: new Promise((resolve) => { complete = resolve; }), - cancel: () => {}, + cancel: () => undefined, }; }, onSubmit: async (values, _setPhase, o) => { @@ -830,14 +832,14 @@ describe("runProviderSetup sign-in", () => { const settingsPath = join(dir, "settings.json"); const localPath = localSettingsPath(dir); let commits = 0; - let complete: (result: LoginCompletion) => void = () => {}; + let complete: (result: LoginCompletion) => void = () => undefined; const { done, harness } = await mountLogin({ start: async () => ({ authorizeUrl: AUTHORIZE_URL, completed: new Promise((resolve) => { complete = resolve; }), - cancel: () => {}, + cancel: () => undefined, }), onSubmit: async (values, _setPhase, opts) => { if (opts.oauth === undefined) throw new Error("expected staged OAuth credentials"); @@ -888,8 +890,8 @@ describe("runProviderSetup sign-in", () => { seenProfiles.push(profile); return { authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), - cancel: () => {}, + completed: new Promise(() => undefined), + cancel: () => undefined, }; }, }); @@ -908,8 +910,8 @@ describe("runProviderSetup sign-in", () => { seenProfiles.push(profile); return { authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), - cancel: () => {}, + completed: new Promise(() => undefined), + cancel: () => undefined, }; }, }); @@ -933,8 +935,8 @@ describe("runProviderSetup sign-in", () => { seenProfiles.push(profile); return { authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), - cancel: () => {}, + completed: new Promise(() => undefined), + cancel: () => undefined, }; }, }); @@ -967,8 +969,8 @@ describe("runProviderSetup sign-in", () => { starts += 1; return { authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), - cancel: () => {}, + completed: new Promise(() => undefined), + cancel: () => undefined, }; }, }); @@ -998,8 +1000,8 @@ describe("runProviderSetup sign-in", () => { starts += 1; return { authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), - cancel: () => {}, + completed: new Promise(() => undefined), + cancel: () => undefined, }; }, }); @@ -1025,8 +1027,8 @@ describe("runProviderSetup sign-in", () => { completed: starts === 1 ? Promise.reject(new Error("access denied by the user")) - : new Promise(() => {}), - cancel: () => {}, + : new Promise(() => undefined), + cancel: () => undefined, }; }, }); @@ -1050,7 +1052,7 @@ describe("runProviderSetup sign-in", () => { loginTimeoutMs: 5, start: async () => ({ authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), + completed: new Promise(() => undefined), cancel: () => { cancelled += 1; }, @@ -1078,7 +1080,7 @@ describe("runProviderSetup sign-in", () => { }); return { authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), + completed: new Promise(() => undefined), cancel: () => { cancelled += 1; }, @@ -1108,8 +1110,8 @@ describe("runProviderSetup sign-in", () => { completed: seenProfiles.length === 1 ? Promise.reject(new Error("access denied by the user")) - : new Promise(() => {}), - cancel: () => {}, + : new Promise(() => undefined), + cancel: () => undefined, }; }, }); @@ -1130,14 +1132,14 @@ describe("runProviderSetup sign-in", () => { }); test("a late resolution from an abandoned attempt cannot move the screen", async () => { - let complete: (result: LoginCompletion) => void = () => {}; + let complete: (result: LoginCompletion) => void = () => undefined; const { done, harness } = await mountLogin({ start: async () => ({ authorizeUrl: AUTHORIZE_URL, completed: new Promise((resolve) => { complete = resolve; }), - cancel: () => {}, + cancel: () => undefined, }), }); await pickRow(harness, PROVIDER_IDS, "codex"); @@ -1254,7 +1256,7 @@ describe("runProviderSetup", () => { }); test("shows the telemetry notice only when asked to", async () => { - const shown = await mountSetup(async () => {}, true); + const shown = await mountSetup(async () => undefined, true); await shown.harness.renderOnce(); expect(shown.harness.captureCharFrame()).toContain("telemetry"); shown.harness.pressKey("Ctrl+C"); @@ -1322,8 +1324,8 @@ describe("runProviderSetup", () => { }); test("reports the submit phase while onSubmit runs", async () => { - let advance: (phase: "testing" | "saving") => void = () => {}; - let finish: () => void = () => {}; + let advance: (phase: "testing" | "saving") => void = () => undefined; + let finish: () => void = () => undefined; const { done, harness } = await mountSetup((_values, setPhase) => { advance = setPhase; return new Promise((resolve) => { @@ -1377,7 +1379,7 @@ describe("runProviderSetup", () => { const { done, harness } = await mountLogin({ start: async () => ({ authorizeUrl: AUTHORIZE_URL, - completed: new Promise(() => {}), + completed: new Promise(() => undefined), cancel: () => { cancelled += 1; }, @@ -1520,7 +1522,7 @@ describe("runProviderSetup pick-list height cap", () => { test(`stays within a ${height}-row terminal with no overlapping chrome`, async () => { const harness = await createHarness({ width: 80, height }); runProviderSetup({ - onSubmit: async () => {}, + onSubmit: async () => undefined, showTelemetryNotice: false, createRenderer: async () => harness.renderer, }); @@ -1541,7 +1543,7 @@ describe("runProviderSetup pick-list height cap", () => { test("keyboard navigation scrolls a long provider list and keeps the active row visible", async () => { const harness = await createHarness({ width: 80, height: 16 }); runProviderSetup({ - onSubmit: async () => {}, + onSubmit: async () => undefined, showTelemetryNotice: false, createRenderer: async () => harness.renderer, }); @@ -1553,7 +1555,7 @@ describe("runProviderSetup pick-list height cap", () => { const frame = harness.captureCharFrame(); const last = providerChoiceRows(providerChoices()).at(-1); expect(last).toBeDefined(); - expect(frame).toContain(last!.label.slice(0, 20)); + expect(frame).toContain(defined(last, "last").label.slice(0, 20)); }); // statusLine and guidance are both blank on the first screen these tests diff --git a/src/tui/provider/setup.ts b/src/tui/provider/setup.ts index 8115981d1..6acc3252a 100644 --- a/src/tui/provider/setup.ts +++ b/src/tui/provider/setup.ts @@ -129,7 +129,7 @@ export async function runProviderSetup(config: ProviderSetupConfig): Promise {}, + resolveDone: () => undefined, }; if (config.initialProviderId !== undefined) { diff --git a/src/tui/queued-delivery-hop.test.ts b/src/tui/queued-delivery-hop.test.ts index ed00d195e..949b9e504 100644 --- a/src/tui/queued-delivery-hop.test.ts +++ b/src/tui/queued-delivery-hop.test.ts @@ -18,7 +18,7 @@ function lastHopPort(bridgeRef: { current: SessionBridge | undefined }) { send: (text) => { sends.push(text); }, - interrupt: () => {}, + interrupt: () => undefined, deliver: routeQueuedDelivery({ send: (text) => { sends.push(text); @@ -72,7 +72,7 @@ describe("queued delivery last hop", () => { const sends: string[] = []; const delivered: string[] = []; const { enqueue, awaitTail } = createSessionOperationQueue(); - let resolveSlow!: () => void; + let resolveSlow: () => void = () => undefined; const slow = new Promise((resolve) => { resolveSlow = resolve; }); @@ -80,7 +80,7 @@ describe("queued delivery last hop", () => { send: (text) => { sends.push(text); }, - interrupt: () => {}, + interrupt: () => undefined, deliver: routeQueuedDelivery({ send: (text) => { sends.push(text); diff --git a/src/tui/queued-delivery.test.ts b/src/tui/queued-delivery.test.ts index 78c7155a0..4d271b0fc 100644 --- a/src/tui/queued-delivery.test.ts +++ b/src/tui/queued-delivery.test.ts @@ -128,7 +128,7 @@ describe("createLiveSteerDeliver", () => { test("slow first ingest does not let a later steer deliver first", async () => { const delivered: string[] = []; const { enqueue, awaitTail } = createSessionOperationQueue(); - let resolveSlow!: () => void; + let resolveSlow: () => void = () => undefined; const slow = new Promise((resolve) => { resolveSlow = resolve; }); @@ -162,7 +162,7 @@ describe("createLiveSteerDeliver", () => { const delivered: string[] = []; const { enqueue, awaitTail } = createSessionOperationQueue(); const generation = createDeliveryGeneration(); - let resolveSlow!: () => void; + let resolveSlow: () => void = () => undefined; const slow = new Promise((resolve) => { resolveSlow = resolve; }); @@ -196,7 +196,7 @@ describe("createLeftoverSend", () => { const recorded: string[] = []; const { enqueue, awaitTail } = createSessionOperationQueue(); const generation = createDeliveryGeneration(); - let resolveSlow!: () => void; + let resolveSlow: () => void = () => undefined; const slow = new Promise((resolve) => { resolveSlow = resolve; }); @@ -285,7 +285,7 @@ describe("createLeftoverSend", () => { const enterSent: string[] = []; const { enqueue, awaitTail } = createSessionOperationQueue(); const generation = createDeliveryGeneration(); - let resolveSlow!: () => void; + let resolveSlow: () => void = () => undefined; const slow = new Promise((resolve) => { resolveSlow = resolve; }); diff --git a/src/tui/row-click.test.ts b/src/tui/row-click.test.ts index dad01f30a..94a67c0da 100644 --- a/src/tui/row-click.test.ts +++ b/src/tui/row-click.test.ts @@ -7,6 +7,7 @@ */ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { withTestRenderer } from "./harness"; import { appendStreamRow } from "./shell/chrome"; import { createAppShell } from "./shell/index"; @@ -51,7 +52,7 @@ describe("clicking a row's expand arrow", () => { const arrow = findCell(h.captureCharFrame(), ROW_ARROW.collapsed); expect(arrow).not.toBeNull(); - await h.mockMouse.click(arrow!.x, arrow!.y); + await h.mockMouse.click(defined(arrow).x, defined(arrow).y); await h.renderOnce(); // One row only: the pointer said which. expect(shell.streamLog[0]?.expanded).toBe(true); @@ -59,13 +60,13 @@ describe("clicking a row's expand arrow", () => { const open = findCell(h.captureCharFrame(), ROW_ARROW.expanded); expect(open).not.toBeNull(); - await h.mockMouse.click(open!.x, open!.y); + await h.mockMouse.click(defined(open).x, defined(open).y); await h.renderOnce(); expect(shell.streamLog[0]?.expanded).toBe(false); const text = findCell(h.captureCharFrame(), "apple.com"); expect(text).not.toBeNull(); - await h.mockMouse.click(text!.x, text!.y); + await h.mockMouse.click(defined(text).x, defined(text).y); await h.renderOnce(); expect(shell.streamLog[0]?.expanded).toBe(false); } finally { diff --git a/src/tui/row-retext.test.ts b/src/tui/row-retext.test.ts index 722c78fab..f76eb76f0 100644 --- a/src/tui/row-retext.test.ts +++ b/src/tui/row-retext.test.ts @@ -4,6 +4,7 @@ * pending must dim its gutter on the same node, not keep the live bronze. */ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { BoxRenderable, TextRenderable, @@ -47,7 +48,7 @@ describe("retext gutter voice", () => { name: "fetch", arguments: JSON.stringify({ url: "https://x.dev" }), }); - const pending = rows[0]!; + const pending = defined(rows[0]); expect(pending.pending).toBe(true); appendStreamRow(shell, pending); await h.renderOnce(); @@ -58,7 +59,7 @@ describe("retext gutter voice", () => { expect(fgIs(gutter, UI.textDim)).toBe(false); pushToolResult(rows, { name: "fetch", content: "", isError: true }); - const failed = rows[0]!; + const failed = defined(rows[0]); expect(failed.failed).toBe(true); replaceStreamRowAt(shell, 0, failed); // Same paint node, same shape: the flip retexted rather than rebuilt. diff --git a/src/tui/row-update-perf.test.ts b/src/tui/row-update-perf.test.ts index 59aa87ce4..712349b73 100644 --- a/src/tui/row-update-perf.test.ts +++ b/src/tui/row-update-perf.test.ts @@ -140,7 +140,7 @@ describe("row update perf gates (J3)", () => { now: () => nowMs, schedule: (fn: () => void) => { tick = fn; - return () => {}; + return () => undefined; }, }); try { diff --git a/src/tui/runner-host.test.ts b/src/tui/runner-host.test.ts index 381a43fff..294cb71d2 100644 --- a/src/tui/runner-host.test.ts +++ b/src/tui/runner-host.test.ts @@ -139,15 +139,15 @@ describe("mountRunnerHost session bridge", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -172,15 +172,15 @@ describe("mountRunnerHost chrome wiring", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: () => commands, - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -207,13 +207,13 @@ describe("mountRunnerHost chrome wiring", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ tasks: liveTasks, agents: [] }), subscribeChrome: (n) => { notify = n; @@ -252,15 +252,15 @@ describe("mountRunnerHost command surfaces", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, surfaces: { @@ -271,10 +271,10 @@ describe("mountRunnerHost command surfaces", () => { telemetryEnabled: false, showPromptCost: false, }), - setCompactionMode: () => {}, - setWaitForApproval: () => {}, - setTelemetryEnabled: () => {}, - setShowPromptCost: () => {}, + setCompactionMode: () => undefined, + setWaitForApproval: () => undefined, + setTelemetryEnabled: () => undefined, + setShowPromptCost: () => undefined, }, }, }); @@ -298,16 +298,16 @@ describe("mountRunnerHost model picker", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4", "grok-3"] } }, activeModel: () => ({ provider: "xai", model: "grok-4" }), - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -330,15 +330,15 @@ describe("mountRunnerHost model picker", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4"] } }, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -361,16 +361,16 @@ describe("mountRunnerHost model picker", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4"] } }, - onModelSelect: () => {}, + onModelSelect: () => undefined, onFavoriteToggle: (id) => toggled.push(id), commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -393,16 +393,16 @@ describe("mountRunnerHost model picker", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4"] } }, - onModelSelect: () => {}, + onModelSelect: () => undefined, onSetDefault: (id) => setDefault.push(id), commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -423,20 +423,20 @@ describe("mountRunnerHost model picker", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4"] } }, - onModelSelect: () => {}, + onModelSelect: () => undefined, onConnectProvider: (name) => connected.push(name), addProviderChoices: () => [ { id: "codex", label: "Codex", hint: "", accountCount: 1 }, { id: "openai", label: "OpenAI", hint: "", accountCount: 0 }, ], commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -459,20 +459,20 @@ describe("mountRunnerHost model picker", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4"] } }, - onModelSelect: () => {}, - onConnectProvider: () => {}, + onModelSelect: () => undefined, + onConnectProvider: () => undefined, addProviderChoices: () => [ { id: "codex", label: "Codex", hint: "", accountCount: 1 }, { id: "openai", label: "OpenAI", hint: "", accountCount: 0 }, ], commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -491,15 +491,15 @@ describe("mountRunnerHost model picker", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4"] } }, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); @@ -519,15 +519,15 @@ describe("bottom border cost run", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, readCostSummary: () => fakeCostSummary(), @@ -548,15 +548,15 @@ describe("bottom border cost run", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, readCostSummary: () => fakeCostSummary(), @@ -581,9 +581,9 @@ describe("bottom border cost run", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { xai: { models: ["grok-4"] }, "codex/abk-labs": { models: ["gpt-5.5"] }, @@ -594,9 +594,9 @@ describe("bottom border cost run", () => { provider = id.slice(0, sep); }, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, readCostSummary: () => ({ @@ -631,9 +631,9 @@ describe("bottom border cost run", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: { "codex/abk-labs": { models: ["gpt-5.5"] }, xai: { models: ["grok-4"] }, @@ -644,9 +644,9 @@ describe("bottom border cost run", () => { provider = id.slice(0, sep); }, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, readCostSummary: () => ({ @@ -681,15 +681,15 @@ describe("bottom border cost run", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: emitter, - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, // Stale occupancy — refreshCostContext would re-paint this if clear @@ -717,15 +717,15 @@ describe("bottom border cost run", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: emitter, - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, readCostSummary: () => ({ ...fakeCostSummary(), contextPercentUsed: percent }), @@ -751,15 +751,15 @@ describe("bottom border cost run", () => { const host = await mountRunnerHost({ title: "test", eventEmitter: emitter, - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, readCostSummary: () => ({ ...fakeCostSummary(), contextPercentUsed: percent }), @@ -791,15 +791,15 @@ describe("mountRunnerHost quit key", () => { const baseDeps = (harness: Awaited>) => ({ title: "test", eventEmitter: new EventEmitter(), - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, providers: {}, - onModelSelect: () => {}, + onModelSelect: () => undefined, commands: [], - onCommand: () => {}, + onCommand: () => undefined, chrome: () => ({ agents: [] }), - subscribeChrome: () => () => {}, + subscribeChrome: () => () => undefined, subAgentSessions: () => [], createRenderer: async () => harness.renderer, }); diff --git a/src/tui/runner/exit.test.ts b/src/tui/runner/exit.test.ts index e71e7612b..c0c557d9d 100644 --- a/src/tui/runner/exit.test.ts +++ b/src/tui/runner/exit.test.ts @@ -2,6 +2,7 @@ import { describe, expect, spyOn, test } from "bun:test"; import { getLogger } from "@intx/log"; import { LOG_NAMESPACE_ROOT } from "../../branding.js"; +import { defined } from "../../../tests/helpers/defined.js"; import { finalizeTUIRun } from "./exit.js"; import type { RunnerServices, RunnerState } from "./state.js"; @@ -55,7 +56,7 @@ function stubQuit(args: { awaitTail: () => Promise; shutdownRuntime: () => describe("finalizeTUIRun quit order", () => { test("starts runtime shutdown without waiting on a hung session-op tail", async () => { const order: string[] = []; - let settleTail!: (err: Error) => void; + let settleTail: ((err: Error) => void) | undefined; const hungTail = new Promise((_, reject) => { settleTail = reject; }); @@ -74,7 +75,7 @@ describe("finalizeTUIRun quit order", () => { await new Promise((resolve) => setTimeout(resolve, 50)); expect(order[0]).toBe("shutdown"); } finally { - settleTail(new Error("stop")); + defined(settleTail, "settleTail")(new Error("stop")); } await expect(pending).rejects.toThrow("stop"); }); @@ -82,7 +83,7 @@ describe("finalizeTUIRun quit order", () => { test("logs a runtime shutdown failure instead of swallowing it", async () => { const logger = getLogger([LOG_NAMESPACE_ROOT, "tui"]); const errorSpy = spyOn(logger, "error"); - let settleTail!: (err: Error) => void; + let settleTail: ((err: Error) => void) | undefined; const hungTail = new Promise((_, reject) => { settleTail = reject; }); @@ -104,7 +105,7 @@ describe("finalizeTUIRun quit order", () => { expect(first?.[1]).toEqual({ error: "plugin dispose failed" }); } finally { errorSpy.mockRestore(); - settleTail(new Error("stop")); + defined(settleTail, "settleTail")(new Error("stop")); } await expect(pending).rejects.toThrow("stop"); }); diff --git a/src/tui/runner/session.ts b/src/tui/runner/session.ts index 0bc60f1a2..72d2c2b67 100644 --- a/src/tui/runner/session.ts +++ b/src/tui/runner/session.ts @@ -198,8 +198,9 @@ export async function assembleTUISession( // Attach agent profiles to their descriptors so the /plugins UI can show // which sub-agents a plugin contributes. for (const mod of pluginState.modules) { - if (mod.manifest?.kind !== "agent" || mod.agentPlugin === undefined) continue; - const desc = pluginState.descriptors.find((d) => d.id === mod.manifest!.id); + const manifest = mod.manifest; + if (manifest?.kind !== "agent" || mod.agentPlugin === undefined) continue; + const desc = pluginState.descriptors.find((d) => d.id === manifest.id); if (desc === undefined) continue; const agents = Array.isArray(mod.agentPlugin.agents) ? mod.agentPlugin.agents : []; desc.agentProfiles = agents diff --git a/src/tui/runner/wiring.ask-wake.test.ts b/src/tui/runner/wiring.ask-wake.test.ts index c7b3c7318..50a9039fe 100644 --- a/src/tui/runner/wiring.ask-wake.test.ts +++ b/src/tui/runner/wiring.ask-wake.test.ts @@ -25,8 +25,8 @@ test("failed reset releases publication without flushing partially cancelled wor store.registerAsk("old", { question: "Old question?", questionId: "old-question", - resolve: () => {}, - reject: () => {}, + resolve: () => undefined, + reject: () => undefined, }); events.length = 0; const error = new Error("reset failed"); @@ -88,7 +88,7 @@ for (const phase of ["settled", "prequeued", "deferred"] as const) { scheduled.push(text); deliver(text); }, - interrupt: () => {}, + interrupt: () => undefined, }), ); let resetting = false; @@ -113,8 +113,8 @@ for (const phase of ["settled", "prequeued", "deferred"] as const) { store.registerAsk(id, { question: `question ${id}`, questionId: `question-${id}`, - resolve: () => {}, - reject: () => {}, + resolve: () => undefined, + reject: () => undefined, }); }; try { @@ -181,7 +181,7 @@ for (const removal of ["answer", "cancel", "terminal", "remove", "replace"] as c }; const bridge = attachSessionBridge( shell, - createLiveSessionPort({ send, deliver: send, interrupt: () => {} }), + createLiveSessionPort({ send, deliver: send, interrupt: () => undefined }), ); const store = createSubAgentSessionStore(); const emitter = new EventEmitter(); @@ -204,8 +204,8 @@ for (const removal of ["answer", "cancel", "terminal", "remove", "replace"] as c store.registerAsk(worker.id, { question: "Which port?", questionId: "q1", - resolve: () => {}, - reject: () => {}, + resolve: () => undefined, + reject: () => undefined, }); if (removal === "answer") { const mailbox = createFleetMailbox(store); @@ -264,7 +264,7 @@ test("same catalog workers answer by session, reconcile one resolution and repla }; const bridge = attachSessionBridge( shell, - createLiveSessionPort({ send, deliver: send, interrupt: () => {} }), + createLiveSessionPort({ send, deliver: send, interrupt: () => undefined }), ); const store = createSubAgentSessionStore(); const emitter = new EventEmitter(); @@ -279,7 +279,7 @@ test("same catalog workers answer by session, reconcile one resolution and repla resolve: (answer) => { answers.push(`${id}:${answer}`); }, - reject: () => {}, + reject: () => undefined, }); try { bridge.handle({ type: "inference.start", data: {} }); diff --git a/src/tui/runtime-bridge-coalesce.test.ts b/src/tui/runtime-bridge-coalesce.test.ts index 4615aa2cf..d74971298 100644 --- a/src/tui/runtime-bridge-coalesce.test.ts +++ b/src/tui/runtime-bridge-coalesce.test.ts @@ -52,7 +52,7 @@ describe("runtime-bridge stream row coalescing", () => { const clock = { ms: 1000 }; const bridge = attachSessionBridge(shell, createRecordingPort(), { now: () => clock.ms, - schedule: () => () => {}, + schedule: () => () => undefined, }); try { const tokens = ["The ", "quick ", "brown ", "fox ", "jumps."]; @@ -101,7 +101,7 @@ describe("runtime-bridge stream row coalescing", () => { const clock = { ms: 1000 }; const bridge = attachSessionBridge(shell, createRecordingPort(), { now: () => clock.ms, - schedule: () => () => {}, + schedule: () => () => undefined, }); try { const tokens = ["reason ", "one ", "two ", "three."]; diff --git a/src/tui/runtime-bridge.test.ts b/src/tui/runtime-bridge.test.ts index 9d710f662..66b73cec3 100644 --- a/src/tui/runtime-bridge.test.ts +++ b/src/tui/runtime-bridge.test.ts @@ -1,4 +1,5 @@ import { describe, expect, spyOn, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { FIXTURE_BUSY_SESSION, attachSessionBridge, @@ -270,7 +271,7 @@ describe("attachSessionBridge", () => { }); // Soft steer drained; follow-up still pending. expect(badgeCount(shell.session)).toBe(1); - expect(shell.session.items[0]!.kind).toBe("queue"); + expect(defined(shell.session.items[0], "queued item").kind).toBe("queue"); const deliver = port.calls.find((c) => c.op === "deliver"); expect(deliver).toEqual({ op: "deliver", @@ -331,7 +332,7 @@ describe("attachSessionBridge", () => { const port = createRecordingPort(); const bridge = attachSessionBridge(shell, port, { now: () => clock, - schedule: () => () => {}, + schedule: () => () => undefined, }); try { bridge.handle({ @@ -1820,7 +1821,7 @@ describe("syncAgentProgress", () => { expect(streamRowCount(shell)).toBe(rowCountBefore); expect(removeSpy.mock.calls.length).toBeLessThanOrEqual(2); - const row = shell.streamLog[rowCountBefore - 1]!; + const row = defined(shell.streamLog[rowCountBefore - 1], "progress row"); expect(row.pending).toBe(true); expect(row.agentWorking).toBe(true); expect(row.stat).toContain("grep"); @@ -1830,7 +1831,7 @@ describe("syncAgentProgress", () => { taskSession({ currentToolName: "grep", lastActivityAt: 42_000 }), ]); await h.renderOnce(); - const stalledRow = shell.streamLog[rowCountBefore - 1]!; + const stalledRow = defined(shell.streamLog[rowCountBefore - 1], "stalled row"); expect(stalledRow.agentWorking).toBe(false); removeSpy.mockRestore(); @@ -1869,8 +1870,8 @@ describe("syncAgentProgress", () => { }); const index = shell.streamLog.length - 1; bridge.syncAgentProgress([taskSession({ status: "done" })]); - expect(shell.streamLog[index]!.pending).not.toBe(true); - expect(shell.streamLog[index]!.agentWorking).toBeUndefined(); + expect(defined(shell.streamLog[index], "finished row").pending).not.toBe(true); + expect(defined(shell.streamLog[index], "finished row").agentWorking).toBeUndefined(); } finally { bridge.dispose(); shell.dispose(); @@ -1916,7 +1917,7 @@ describe("syncAgentProgress", () => { nowMs = 42_000; bridge.syncAgentProgress([taskSession({ lastActivityAt: nowMs })]); await h.renderOnce(); - const row = shell.streamLog[index]!; + const row = defined(shell.streamLog[index], "live progress row"); expect(row.agentWorking).toBe(true); expect(row.stat).toContain("grep"); } finally { @@ -1956,12 +1957,12 @@ describe("in-flight tool row elapsed time", () => { data: { name: "run_shell", callId: "c1", arguments: "sleep 30" }, }); const index = streamRowCount(shell) - 1; - expect(shell.streamLog[index]!.stat).toBeUndefined(); + expect(defined(shell.streamLog[index], "tool row").stat).toBeUndefined(); nowMs = 65_000; tick?.(); await h.renderOnce(); - expect(shell.streamLog[index]!.stat).toBe("1:05"); + expect(defined(shell.streamLog[index], "tool row").stat).toBe("1:05"); bridge.handle({ type: "tool.done", @@ -1969,7 +1970,7 @@ describe("in-flight tool row elapsed time", () => { }); // The elapsed clock was scaffolding for the wait, not a fact worth // keeping — the answer's own addendum takes the row over. - expect(shell.streamLog[index]!.stat).not.toBe("1:05"); + expect(defined(shell.streamLog[index], "tool row").stat).not.toBe("1:05"); } finally { bridge.dispose(); shell.dispose(); @@ -2009,12 +2010,12 @@ describe("in-flight tool row elapsed time", () => { }, }); const index = streamRowCount(shell) - 1; - const before = shell.streamLog[index]!.stat; + const before = defined(shell.streamLog[index], "diff row").stat; expect(before).toContain("+"); nowMs = 65_000; tick?.(); - expect(shell.streamLog[index]!.stat).toBe(before); + expect(defined(shell.streamLog[index], "diff row").stat).toBe(before); } finally { bridge.dispose(); shell.dispose(); diff --git a/src/tui/runtime-bridge.ts b/src/tui/runtime-bridge.ts index 217965b75..8c0573b58 100644 --- a/src/tui/runtime-bridge.ts +++ b/src/tui/runtime-bridge.ts @@ -236,10 +236,10 @@ export interface SessionBridge { } const NOOP_PORT: SessionPort = { - sendImmediate: () => {}, - enqueue: () => {}, - interrupt: () => {}, - deliver: () => {}, + sendImmediate: () => undefined, + enqueue: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, }; export type PortCall = diff --git a/src/tui/runtime-channels.test.ts b/src/tui/runtime-channels.test.ts index 10a16679d..6c8f5ab40 100644 --- a/src/tui/runtime-channels.test.ts +++ b/src/tui/runtime-channels.test.ts @@ -11,6 +11,7 @@ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { createHarness } from "./harness.js"; import { mountProductHost, type ProductHostConfig } from "./product-host.js"; import { isLanding } from "./shell/internals.js"; @@ -26,9 +27,9 @@ async function mountHeadless(overrides: Partial = {}): Promis const host = await mountProductHost({ title: "test-session", eventEmitter: emitter, - send: () => {}, - interrupt: () => {}, - deliver: () => {}, + send: () => undefined, + interrupt: () => undefined, + deliver: () => undefined, createRenderer: async () => harness.renderer, ...overrides, }); @@ -362,7 +363,9 @@ describe("every emitted runtime channel has a subscriber", () => { .join("\n"); const emitted = new Set( - [...runnerSources.matchAll(/emitter\.emit\("([a-z.]+)"/g)].map((m) => m[1]!), + [...runnerSources.matchAll(/emitter\.emit\("([a-z.]+)"/g)].map((m) => + defined(m[1], "emit channel"), + ), ); // Progress pings are store-mirrored chrome, not a host paint path. emitted.delete("subagent.progress"); diff --git a/src/tui/runtime-shutdown.test.ts b/src/tui/runtime-shutdown.test.ts index c55453337..228dbe31b 100644 --- a/src/tui/runtime-shutdown.test.ts +++ b/src/tui/runtime-shutdown.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { createRuntimeShutdown } from "./runner/shutdown.js"; describe("runtime shutdown", () => { @@ -107,7 +108,7 @@ describe("runtime shutdown", () => { test("awaits an async toolset dispose before resolving", async () => { const calls: string[] = []; - let resolveToolset!: () => void; + let resolveToolset: (() => void) | undefined; const toolsetGate = new Promise((resolve) => { resolveToolset = resolve; }); @@ -128,14 +129,14 @@ describe("runtime shutdown", () => { const pending = shutdown(); await Promise.resolve(); expect(calls).toEqual(["host"]); - resolveToolset(); + defined(resolveToolset, "resolveToolset")(); await pending; expect(calls).toEqual(["host", "toolset", "workers", "agent"]); }); test("reaps the toolset before waiting on a hung agent close", async () => { const calls: string[] = []; - let releaseClose!: () => void; + let releaseClose: (() => void) | undefined; const closeGate = new Promise((resolve) => { releaseClose = resolve; }); @@ -156,7 +157,7 @@ describe("runtime shutdown", () => { const pending = shutdown(); await new Promise((resolve) => setTimeout(resolve, 20)); expect(calls).toEqual(["host", "toolset", "workers"]); - releaseClose(); + defined(releaseClose, "releaseClose")(); await pending; expect(calls).toEqual(["host", "toolset", "workers", "agent"]); }); @@ -168,7 +169,7 @@ describe("runtime shutdown", () => { cancelWorkers: () => undefined, closeAgent: () => { closeStarted = true; - return new Promise(() => {}); + return new Promise(() => undefined); }, disposeToolset: async () => { throw new Error("1 shell child process still live after 2000ms reap"); diff --git a/src/tui/selection-copy.test.ts b/src/tui/selection-copy.test.ts index a330d9642..cfe55f0b3 100644 --- a/src/tui/selection-copy.test.ts +++ b/src/tui/selection-copy.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { createRecordingClipboard } from "./copy-path.js"; import { copyFinishedSelection, type SelectionCopyHost } from "./selection-copy.js"; @@ -71,7 +72,7 @@ describe("copyFinishedSelection", () => { }); expect(h.clipboard.writes).toEqual([long]); expect(h.flashes[0]).toContain("…"); - expect(h.flashes[0]!.length).toBeLessThan(long.length + 40); + expect(defined(h.flashes[0]).length).toBeLessThan(long.length + 40); }); test("collapses multi-line selections in the flash preview", () => { @@ -87,7 +88,7 @@ describe("copyFinishedSelection", () => { }); test("clears highlight immediately while write is still pending", async () => { - let resolveWrite!: () => void; + let resolveWrite: () => void = () => undefined; const writeP = new Promise((r) => { resolveWrite = r; }); diff --git a/src/tui/sent-message-history.test.ts b/src/tui/sent-message-history.test.ts index cb7cfdb60..40d056129 100644 --- a/src/tui/sent-message-history.test.ts +++ b/src/tui/sent-message-history.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { createSentHistoryBrowse, stepSentHistoryDown, @@ -22,40 +23,40 @@ describe("sent-message-history", () => { test("up walks to older messages", () => { let browse = createSentHistoryBrowse(sent); - browse = stepSentHistoryUp(browse, "")!.browse; + browse = defined(stepSentHistoryUp(browse, "")).browse; const step = stepSentHistoryUp(browse, "third"); expect(step?.value).toBe("second"); - browse = step!.browse; + browse = defined(step).browse; const oldest = stepSentHistoryUp(browse, "second"); expect(oldest?.value).toBe("first"); - expect(stepSentHistoryUp(oldest!.browse, "first")).toBeNull(); + expect(stepSentHistoryUp(defined(oldest).browse, "first")).toBeNull(); }); test("down from oldest returns through newer to draft", () => { let browse = createSentHistoryBrowse(sent); - browse = stepSentHistoryUp(browse, "my draft")!.browse; - browse = stepSentHistoryUp(browse, "third")!.browse; - browse = stepSentHistoryUp(browse, "second")!.browse; + browse = defined(stepSentHistoryUp(browse, "my draft")).browse; + browse = defined(stepSentHistoryUp(browse, "third")).browse; + browse = defined(stepSentHistoryUp(browse, "second")).browse; const toSecond = stepSentHistoryDown(browse, "first", 5); expect(toSecond?.value).toBe("second"); - const toThird = stepSentHistoryDown(toSecond!.browse, "second", 6); + const toThird = stepSentHistoryDown(defined(toSecond).browse, "second", 6); expect(toThird?.value).toBe("third"); - const toDraft = stepSentHistoryDown(toThird!.browse, "third", 5); + const toDraft = stepSentHistoryDown(defined(toThird).browse, "third", 5); expect(toDraft?.value).toBe("my draft"); expect(toDraft?.browse.browseIndex).toBeNull(); }); test("editing exits browse mode", () => { - const browse = stepSentHistoryUp(createSentHistoryBrowse(sent), "x")!.browse; + const browse = defined(stepSentHistoryUp(createSentHistoryBrowse(sent), "x")).browse; expect(sentHistoryOnEdit(browse).browseIndex).toBeNull(); }); test("up from browse index with cursor at end still reaches older messages", () => { let browse = createSentHistoryBrowse(sent); - browse = stepSentHistoryUp(browse, "")!.browse; + browse = defined(stepSentHistoryUp(browse, "")).browse; expect(browse.browseIndex).toBe(0); const older = stepSentHistoryUp(browse, "third"); expect(older?.value).toBe("second"); diff --git a/src/tui/sent-message-history.ts b/src/tui/sent-message-history.ts index 51c536680..d947c1ae6 100644 --- a/src/tui/sent-message-history.ts +++ b/src/tui/sent-message-history.ts @@ -45,7 +45,8 @@ export function stepSentHistoryUp( draft: currentValue, browseIndex: 0, }; - const value = browse.sent[browse.sent.length - 1]!; + const value = browse.sent[browse.sent.length - 1]; + if (value == null) return null; return { browse: next, value, cursor: value.length }; } @@ -58,7 +59,8 @@ export function stepSentHistoryUp( }; const idx = next.browseIndex; if (idx === null) return null; - const value = browse.sent[browse.sent.length - 1 - idx]!; + const value = browse.sent[browse.sent.length - 1 - idx]; + if (value == null) return null; return { browse: next, value, cursor: value.length }; } @@ -75,7 +77,8 @@ export function stepSentHistoryDown( const next: SentHistoryBrowse = { ...browse, browseIndex: browse.browseIndex - 1 }; const idx = next.browseIndex; if (idx === null) return null; - const value = browse.sent[browse.sent.length - 1 - idx]!; + const value = browse.sent[browse.sent.length - 1 - idx]; + if (value == null) return null; return { browse: next, value, cursor: value.length }; } diff --git a/src/tui/session-chrome.test.ts b/src/tui/session-chrome.test.ts index 4d841316a..5269aea90 100644 --- a/src/tui/session-chrome.test.ts +++ b/src/tui/session-chrome.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { ACTIVITY_STATES, classifyAgentSendFailure, @@ -50,7 +51,7 @@ describe("resolveTurnLabel closed-set guarantee", () => { null, ); expect(label).not.toBe(currentToolName); - expect(ACTIVITY_STATES).toContain(label!); + expect(ACTIVITY_STATES).toContain(defined(label)); }); } @@ -67,7 +68,7 @@ describe("resolveTurnLabel closed-set guarantee", () => { ); // Recovery is silent — never paint "stalled" in the ticker. expect(label).toBe("building"); - expect(ACTIVITY_STATES).toContain(label!); + expect(ACTIVITY_STATES).toContain(defined(label)); }); test("waiting on the operator is distinguishable from working", () => { @@ -83,7 +84,7 @@ describe("resolveTurnLabel closed-set guarantee", () => { ); expect(label).toBe("waiting"); expect(label).not.toBe("working"); - expect(ACTIVITY_STATES).toContain(label!); + expect(ACTIVITY_STATES).toContain(defined(label)); }); }); @@ -286,7 +287,7 @@ describe("fleet state in the top-level indicator", () => { test("a healthy fleet reads as working, not the parent's own tool", () => { const label = resolveTurnLabel(parentAwaitingChildren, false, fleet(6, 0)); expect(label).toBe("working"); - expect(ACTIVITY_STATES).toContain(label!); + expect(ACTIVITY_STATES).toContain(defined(label)); }); test("a quiet fleet still reads working at the top level", () => { diff --git a/src/tui/session-operation-queue.test.ts b/src/tui/session-operation-queue.test.ts index 97e46af7b..e8a0d061d 100644 --- a/src/tui/session-operation-queue.test.ts +++ b/src/tui/session-operation-queue.test.ts @@ -5,7 +5,7 @@ test("serial operation queue executes operations in order without interleaving", const log: string[] = []; const { enqueue, awaitTail } = createSessionOperationQueue(); - let resolveA!: () => void; + let resolveA: () => void = () => undefined; const opA = new Promise((r) => (resolveA = r)); enqueue(async () => { diff --git a/src/tui/session-queue.test.ts b/src/tui/session-queue.test.ts index e22a96ecf..0a86b5b36 100644 --- a/src/tui/session-queue.test.ts +++ b/src/tui/session-queue.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { badgeCount, cancelLast, @@ -28,7 +29,7 @@ describe("session-queue", () => { s = enqueue(s, "world"); expect(badgeCount(s)).toBe(2); expect(s.items.map((i) => i.kind)).toEqual(["queue", "queue"]); - expect(s.items[0]!.text).toBe("hello"); + expect(defined(s.items[0]).text).toBe("hello"); }); test("steer and follow-up counts are distinct", () => { @@ -38,7 +39,7 @@ describe("session-queue", () => { expect(badgeCount(s)).toBe(2); expect(steerCount(s)).toBe(1); expect(queueCount(s)).toBe(1); - expect(s.items[1]!.kind).toBe("steer"); + expect(defined(s.items[1]).kind).toBe("steer"); }); test("drain order: steers before queue", () => { @@ -99,7 +100,7 @@ describe("session-queue", () => { const { state, item } = cancelLast(s); expect(item?.text).toBe("drop"); expect(badgeCount(state)).toBe(1); - expect(state.items[0]!.text).toBe("keep"); + expect(defined(state.items[0]).text).toBe("keep"); }); test("cancelLast retracts the newest steer item, same as queue", () => { @@ -110,7 +111,7 @@ describe("session-queue", () => { expect(item?.kind).toBe("steer"); expect(item?.text).toBe("steered"); expect(badgeCount(state)).toBe(1); - expect(state.items[0]!.kind).toBe("queue"); + expect(defined(state.items[0]).kind).toBe("queue"); }); test("cancelLast on an empty queue is a no-op", () => { diff --git a/src/tui/session-start.ts b/src/tui/session-start.ts index ccea72854..488773c68 100644 --- a/src/tui/session-start.ts +++ b/src/tui/session-start.ts @@ -83,11 +83,11 @@ export function createTUICrashGuard(getLiveSession: () => TUILiveSession): TUICr let finalized = false; // Bound after the cycle recorder exists (it needs the session workdir); the // crash guard is declared first so it covers every fallible step below. - let flushPartialOnCrash: () => Promise = async () => {}; + let flushPartialOnCrash: () => Promise = async () => undefined; // Bound once the host is mounted. Without this the crash path leaves the // renderer alive, so the alternate screen, mouse reporting and raw mode are // never disabled and the operator's terminal is left wedged. - let disposeHost: () => void | Promise = () => {}; + let disposeHost: () => void | Promise = () => undefined; let getSession = getLiveSession; const finalizeOnCrash = async (err: unknown): Promise => { diff --git a/src/tui/shell.test.ts b/src/tui/shell.test.ts index 9beb2f64e..20c6ce389 100644 --- a/src/tui/shell.test.ts +++ b/src/tui/shell.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, test } from "bun:test"; import type { KeyEvent } from "@opentui/core"; +import { defined } from "../../tests/helpers/defined.js"; import { IDLE_TRANSCRIPT_FLOOR } from "./geometry/index"; import { focusOwner, scrollLease } from "./focus/index"; import { withTestRenderer } from "./harness"; @@ -168,7 +169,7 @@ describe("createAppShell", () => { // would pass even if the renderer never routed the event here. const rows = h.captureCharFrame().split("\n"); const borderRow = rows.findIndex((r) => r.includes("╭")); - const promptX = rows[borderRow]!.indexOf("╭") + 2; + const promptX = defined(rows[borderRow]).indexOf("╭") + 2; const promptY = borderRow + 1; for (let i = 0; i < 5; i++) { @@ -252,20 +253,20 @@ describe("createAppShell", () => { h.pressKey("Enter"); await h.renderOnce(); - const enter = captured.at(-1)!; + const enter = defined(captured.at(-1)); expect(enter.name === "return" || enter.name === "enter").toBe(true); expect(enter.ctrl).toBe(false); expect(enter.meta).toBe(false); h.pressKey("Alt+Enter"); await h.renderOnce(); - const alt = captured.at(-1)!; + const alt = defined(captured.at(-1)); expect(alt.name === "return" || alt.name === "enter").toBe(true); expect(alt.meta === true || alt.option === true).toBe(true); h.pressKey("Ctrl+C"); await h.renderOnce(); - const ctrlC = captured.at(-1)!; + const ctrlC = defined(captured.at(-1)); expect(ctrlC.name).toBe("c"); expect(ctrlC.ctrl).toBe(true); } finally { @@ -395,7 +396,7 @@ describe("product skin: stream + queue + overlay", () => { shell.prompt.value = "queue me"; submitPrompt(shell, "queue"); expect(shell.pendingQueue).toBe(1); - expect(shell.session.items[0]!.kind).toBe("queue"); + expect(defined(shell.session.items[0]).kind).toBe("queue"); expect(shell.prompt.value).toBe(""); await h.renderOnce(); expect(h.captureCharFrame()).toContain("follow-up 1"); @@ -419,7 +420,7 @@ describe("product skin: stream + queue + overlay", () => { shell.prompt.value = "steer me"; submitPrompt(shell, "steer"); expect(shell.pendingQueue).toBe(1); - expect(shell.session.items[0]!.kind).toBe("steer"); + expect(defined(shell.session.items[0]).kind).toBe("steer"); await h.renderOnce(); await h.renderOnce(); const frame = h.captureCharFrame(); @@ -498,7 +499,7 @@ describe("product skin: stream + queue + overlay", () => { applyShellCancelLast(shell); expect(shell.pendingQueue).toBe(1); - expect(shell.session.items[0]!.text).toBe("keep this one"); + expect(defined(shell.session.items[0]).text).toBe("keep this one"); const after = shell.streamLog.map((row) => ({ text: row.text, @@ -539,7 +540,7 @@ describe("product skin: stream + queue + overlay", () => { shell.prompt.value = "steer me now"; submitPrompt(shell, "steer"); expect(shell.pendingQueue).toBe(1); - expect(shell.session.items[0]!.kind).toBe("steer"); + expect(defined(shell.session.items[0]).kind).toBe("steer"); applyShellCancelLast(shell); diff --git a/src/tui/shell/chrome.ts b/src/tui/shell/chrome.ts index e844702ba..9d2a36b18 100644 --- a/src/tui/shell/chrome.ts +++ b/src/tui/shell/chrome.ts @@ -923,8 +923,9 @@ function paintAppendStreamRow(shell: AppShell, row: StreamRow): void { * mis-truncating the tail that replaced it. */ export function truncateStreamRows(shell: AppShell, length: number): void { - const observing = shell.observe !== null && shell.parentStreamLog !== null; - const log = observing ? shell.parentStreamLog! : shell.streamLog; + const parentLog = shell.parentStreamLog; + const observing = shell.observe !== null && parentLog !== null; + const log = observing ? parentLog : shell.streamLog; const base = observing ? (shell.parentStreamLogBase ?? 0) : shell.streamLogBase; const local = length - base; if (local < 0 || local >= log.length) return; diff --git a/src/tui/shell/transcript.ts b/src/tui/shell/transcript.ts index 76b0b4dfc..8d1e26fdc 100644 --- a/src/tui/shell/transcript.ts +++ b/src/tui/shell/transcript.ts @@ -127,13 +127,15 @@ export const evictionMarkers = new WeakSet(); */ export function transcriptRowChildren(shell: AppShell): readonly BaseRenderable[] { const children = shell.transcript.getChildren().slice(1); - return children.length > 0 && evictionMarkers.has(children[0]!) ? children.slice(1) : children; + const first = children[0]; + return first != null && evictionMarkers.has(first) ? children.slice(1) : children; } /** The eviction-notice node, if the retention cap has dropped anything. */ export function transcriptMarker(shell: AppShell): BaseRenderable | undefined { const children = shell.transcript.getChildren().slice(1); - return children.length > 0 && evictionMarkers.has(children[0]!) ? children[0] : undefined; + const first = children[0]; + return first != null && evictionMarkers.has(first) ? first : undefined; } /** Raw child-list offset before the first row: the spacer, plus the notice if present. */ diff --git a/src/tui/slash-popup-gate.test.ts b/src/tui/slash-popup-gate.test.ts index 320134ccb..eedf34576 100644 --- a/src/tui/slash-popup-gate.test.ts +++ b/src/tui/slash-popup-gate.test.ts @@ -235,7 +235,7 @@ describe("/ popup keeps a queued gate queued across a filter refresh", () => { await withShell(async ({ shell, press }) => { const emitter = new EventEmitter(); const dispose = wireGates(emitter, shell); - const disposeClosedSpy = onOverlayClosed(shell, () => {}); + const disposeClosedSpy = onOverlayClosed(shell, () => undefined); try { press("/"); press("m"); diff --git a/src/tui/stream.test.ts b/src/tui/stream.test.ts index 2ff671fca..5bc5e7823 100644 --- a/src/tui/stream.test.ts +++ b/src/tui/stream.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { stringWidth } from "./view/height"; import { agentVoicesIn, @@ -257,7 +258,7 @@ describe("tool row sentence treatment", () => { test("reads as verb + coloured subject, not tool name + raw args", () => { const row: StreamRow = { role: "tool", text: "{}", verb: "Read", summary: "package.json" }; - const line = toolSentenceLines(row)[0]!; + const line = defined(toolSentenceLines(row)[0]); expect(flatten(row)).toContain("Read"); expect(flatten(row)).toContain("package.json"); const subjectSeg = line.find((seg) => seg.text.includes("package.json")); @@ -314,7 +315,7 @@ describe("tool row sentence treatment", () => { expect(collapsedLines.length).toBe(1); const expandedLines = toolRowLines(row); expect(expandedLines.length).toBe(2); - const tail = expandedLines[1]!; + const tail = defined(expandedLines[1]); expect(tail[0]?.text).toBe(" "); expect(tail.map((s) => s.text).join("")).toContain("+ hello"); }); diff --git a/src/tui/submit-handler.test.ts b/src/tui/submit-handler.test.ts index 79efb1060..1cc0346a8 100644 --- a/src/tui/submit-handler.test.ts +++ b/src/tui/submit-handler.test.ts @@ -225,7 +225,7 @@ describe("image attachment submits", () => { function attachmentHarness() { const sends: { text: string; attachments?: readonly PendingImageAttachment[] }[] = []; const submit = createSubmitHandler({ - dispatchCommand: () => {}, + dispatchCommand: () => undefined, sendPrompt: (text, attachments) => sends.push({ text, ...(attachments ? { attachments } : {}) }), }); diff --git a/src/tui/syntax-highlight.ts b/src/tui/syntax-highlight.ts index 2fa93a848..21aa490f2 100644 --- a/src/tui/syntax-highlight.ts +++ b/src/tui/syntax-highlight.ts @@ -99,7 +99,9 @@ function tokensToLines(tokens: Token[]): StyledSegment[][] { if (part.length === 0) return; const seg: StyledSegment = { text: part, code: true }; if (token.role !== undefined) seg.color = color(token.role); - lines[lines.length - 1]!.push(seg); + const line = lines[lines.length - 1]; + if (line == null) throw new Error("highlight line missing"); + line.push(seg); }); } return lines; diff --git a/src/tui/thinking-reveal.test.ts b/src/tui/thinking-reveal.test.ts index 103cd51b2..6322ec448 100644 --- a/src/tui/thinking-reveal.test.ts +++ b/src/tui/thinking-reveal.test.ts @@ -107,7 +107,8 @@ describe("thinkingLivePreviewLines with a reveal position", () => { const chars = advanceRevealChars(0, sample.length, ms, rate); return thinkingLivePreviewLines(sample, 30, chars); }); - console.log(`rate=${rate}/s`, frames); + expect(frames).toHaveLength(4); + expect(frames.every((row) => row.length > 0)).toBe(true); } expect(true).toBe(true); }); diff --git a/src/tui/tool-execution-watchdog.test.ts b/src/tui/tool-execution-watchdog.test.ts index fc10cfbe0..a94e3be74 100644 --- a/src/tui/tool-execution-watchdog.test.ts +++ b/src/tui/tool-execution-watchdog.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import type { AgentTool } from "@intx/agent"; +import { defined } from "../../tests/helpers/defined.js"; import { createDynamicToolRunner } from "./dynamic-tool-runner.js"; import { DEFAULT_MCP_TOOL_TIMEOUT_MS, @@ -274,7 +275,7 @@ describe("tool execution watchdog", () => { [ stringTool( "mcp__linear__get_issue", - () => new Promise(() => {}), // never resolves — wedged server + () => new Promise(() => undefined), // never resolves — wedged server ), ], { mcpTimeoutMs: 30 }, @@ -291,7 +292,7 @@ describe("tool execution watchdog", () => { test("concurrent mcp tool calls each time out independently", async () => { const runner = createDynamicToolRunner( [ - stringTool("mcp__linear__get_issue", () => new Promise(() => {})), + stringTool("mcp__linear__get_issue", () => new Promise(() => undefined)), stringTool("mcp__linear__list_issues", async () => "ok"), ], { mcpTimeoutMs: 30 }, @@ -393,7 +394,7 @@ describe("tool execution watchdog", () => { 5_000, async () => { // Never resolve within grace. - await new Promise(() => {}); + await new Promise(() => undefined); return { callId: "5", content: "ok" }; }, { salvageGraceMs: TEST_SALVAGE_GRACE_MS, waitForApproval: true }, @@ -434,7 +435,7 @@ describe("tool execution watchdog", () => { parent.signal, undefined, async () => { - await new Promise(() => {}); + await new Promise(() => undefined); return { callId: "unbounded-hang", content: "ok" }; }, { salvageGraceMs: TEST_SALVAGE_GRACE_MS, waitForApproval: true }, @@ -502,10 +503,10 @@ describe("tool execution watchdog", () => { async () => { const budget = getToolApprovalBudget(); expect(budget).toBeDefined(); - const token = budget!.pause(); + const token = defined(budget).pause(); // Longer than the budget — would time out if not paused. await new Promise((r) => setTimeout(r, 120)); - budget!.resume(token); + defined(budget).resume(token); return { callId: "pause", content: "approved-late" }; }, { salvageGraceMs: TEST_SALVAGE_GRACE_MS, waitForApproval: true }, @@ -524,11 +525,11 @@ describe("tool execution watchdog", () => { async () => { const budget = getToolApprovalBudget(); expect(budget).toBeDefined(); - const token = budget!.pause(); + const token = defined(budget).pause(); // Simulate UI thread: resume via captured methods outside this ALS tick. await new Promise((resolve) => { setTimeout(() => { - budget!.resume(token); + defined(budget).resume(token); resolve(); }, 120); }); @@ -620,10 +621,10 @@ describe("tool execution watchdog", () => { async () => { const budget = getToolApprovalBudget(); expect(budget).toBeDefined(); - const token = budget!.pause(); + const token = defined(budget).pause(); // Longer than the outer budget — outer must be frozen too. await new Promise((r) => setTimeout(r, 120)); - budget!.resume(token); + defined(budget).resume(token); return { callId: "inner", content: "child-ok" }; }, { salvageGraceMs: TEST_SALVAGE_GRACE_MS, waitForApproval: true }, diff --git a/src/tui/tool-execution-watchdog.ts b/src/tui/tool-execution-watchdog.ts index 63a76a79b..87af61dbd 100644 --- a/src/tui/tool-execution-watchdog.ts +++ b/src/tui/tool-execution-watchdog.ts @@ -177,7 +177,7 @@ function withParentAbort(signal: AbortSignal): PauseableTimeout { signal.removeEventListener("abort", onParentAbort); }, pause: (): PauseToken => 0, - resume: (_token: PauseToken) => {}, + resume: (_token: PauseToken) => undefined, }; } @@ -407,7 +407,7 @@ export async function runWithToolExecutionWatchdog( : { ...withTimeout(parentSignal, timeoutMs), pause: (): PauseToken => 0, - resume: (_token: PauseToken) => {}, + resume: (_token: PauseToken) => undefined, }; // Nested runs (wait_agents → child tool call) shadow the parent store: the // gate captures the innermost budget, so pause/resume must chain outward or @@ -437,7 +437,7 @@ export async function runWithToolExecutionWatchdog( const salvaged = await preferExecuteSalvageAfterAbort(executePromise, salvageGraceMs); if (salvaged !== undefined) return salvaged; // Avoid unhandled rejection if execute later fails after we move on. - void executePromise.catch(() => {}); + void executePromise.catch(() => undefined); const content = timeoutMs !== undefined && !parentSignal.aborted ? formatTimeoutMessage(call.name, timeoutMs) diff --git a/src/tui/tool-formatter.ts b/src/tui/tool-formatter.ts index 441667629..38dbe9192 100644 --- a/src/tui/tool-formatter.ts +++ b/src/tui/tool-formatter.ts @@ -68,7 +68,11 @@ export function humanizeToolName(toolName: string): string { return toolName .split(/[_\s]+/) .filter((word) => word.length > 0) - .map((word) => word[0]!.toUpperCase() + word.slice(1)) + .map((word) => { + const first = word[0]; + if (first == null) return word; + return first.toUpperCase() + word.slice(1); + }) .join(" "); } @@ -136,9 +140,10 @@ export function describeToolCall(toolName: string, rawArgs: string): ToolCallDes // subject so the row never falls through to raw argument JSON. const prompt = (taskParsed.prompt ?? "").trim(); const subject = description.length > 0 ? description : prompt; + const first = agentName?.[0]; const display = - agentName !== undefined && agentName.length > 0 - ? agentName[0]!.toUpperCase() + agentName.slice(1) + first != null && agentName !== undefined + ? first.toUpperCase() + agentName.slice(1) : "Worker"; // Collapsed row uses the abbreviated subject; Alt+E expands to the full text. return { @@ -453,11 +458,14 @@ function summarizeTaskResultPreview(content: string): string { const body = (reported?.[2] ?? trimmed).trim(); const summarySection = body.match(/^##\s+Summary\s*\n([\s\S]*?)(?=\n##\s|\s*$)/im); if (summarySection) { - const first = summarySection[1]! - .split("\n") - .map((l) => l.trim()) - .find((l) => l.length > 0); - if (first !== undefined && first.length > 0) return abbreviate(first, 64); + const section = summarySection[1]; + if (section != null) { + const first = section + .split("\n") + .map((l) => l.trim()) + .find((l) => l.length > 0); + if (first !== undefined && first.length > 0) return abbreviate(first, 64); + } } const withoutHeadings = body .split("\n") diff --git a/src/tui/tool-rows.test.ts b/src/tui/tool-rows.test.ts index 45bc14393..805cccf40 100644 --- a/src/tui/tool-rows.test.ts +++ b/src/tui/tool-rows.test.ts @@ -4,6 +4,7 @@ */ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { toolCallRow } from "./diff"; import { withTestRenderer } from "./harness"; import { attachSessionBridge, createRecordingPort } from "./runtime-bridge"; @@ -47,7 +48,7 @@ describe("a call and its answer", () => { // The subject stays the call; the answer adds only a certain count. expect(rows[0]?.verb).toBe("Linear: List Issues"); expect(rows[0]?.stat).toBe("2 results"); - expect(painted(rows[0]!)).not.toContain("└"); + expect(painted(defined(rows[0]))).not.toContain("└"); }); test("keep the call as the subject, never the payload", () => { @@ -82,7 +83,7 @@ describe("a call and its answer", () => { pushToolResult(rows, { name: "fetch", content: "connection refused", isError: true }); expect(rows.length).toBe(1); expect(rows[0]?.failed).toBe(true); - expect(painted(rows[0]!)).toContain("×"); + expect(painted(defined(rows[0]))).toContain("×"); expect(rows[0]?.detail?.length).toBeGreaterThan(0); }); @@ -92,7 +93,7 @@ describe("a call and its answer", () => { name: "spawn_agent", arguments: JSON.stringify({ description: "Review mouse/paste" }), }); - rows[0] = { ...rows[0]!, agentWorking: true, stat: "0:42 · bash" }; + rows[0] = { ...defined(rows[0]), agentWorking: true, stat: "0:42 · bash" }; pushToolResult(rows, { name: "spawn_agent", content: "8 lines" }); expect(rows[0]?.pending).toBeUndefined(); @@ -218,7 +219,7 @@ describe("parallel calls to the same tool", () => { callId: "c1", }); expect(rows[0]?.failed).toBe(true); - expect(isCollapsibleRow(rows[0]!)).toBe(true); + expect(isCollapsibleRow(defined(rows[0]))).toBe(true); expect(rows[0]?.detail?.[0]?.[0]?.text).toContain("boom"); }); }); @@ -234,7 +235,7 @@ describe("a long subject", () => { }); const lines = toolSentenceLines(row, 40); expect(lines.length).toBe(1); - const text = lines[0]!.map((segment) => segment.text).join(""); + const text = defined(lines[0]).map((segment) => segment.text).join(""); expect(text.length).toBeLessThanOrEqual(40); expect(text).toContain("…"); }); diff --git a/src/tui/turn-monitor.test.ts b/src/tui/turn-monitor.test.ts index 899798c76..42fd67db2 100644 --- a/src/tui/turn-monitor.test.ts +++ b/src/tui/turn-monitor.test.ts @@ -290,7 +290,7 @@ describe("quota auto-retry", () => { flashSchedule: (fn, ms) => { expect(ms).toBe(RUNTIME_FLASH_MS); lapse.push(fn); - return () => {}; + return () => undefined; }, }); const port = createRecordingPort(); diff --git a/src/tui/turns-to-blocks.ts b/src/tui/turns-to-blocks.ts index 51b1776e8..995233d5e 100644 --- a/src/tui/turns-to-blocks.ts +++ b/src/tui/turns-to-blocks.ts @@ -244,7 +244,9 @@ export function turnsToContentBlocks( const collected: ContentBlockData[][] = []; let total = 0; for (let i = turns.length - 1; i >= 0; i--) { - const blocks = turnToContentBlocks(turns[i]!); + const turn = turns[i]; + if (turn == null) continue; + const blocks = turnToContentBlocks(turn); if (blocks.length === 0) continue; collected.push(blocks); total += blocks.length; @@ -253,7 +255,9 @@ export function turnsToContentBlocks( const out: ContentBlockData[] = []; for (let i = collected.length - 1; i >= 0; i--) { - out.push(...collected[i]!); + const group = collected[i]; + if (group == null) continue; + out.push(...group); } if (out.length > maxBlocks) out.splice(0, out.length - maxBlocks); diff --git a/src/tui/view/height.ts b/src/tui/view/height.ts index 49411746a..af72b8e71 100644 --- a/src/tui/view/height.ts +++ b/src/tui/view/height.ts @@ -45,7 +45,9 @@ export function prefixIndexForWidth(text: string, width: number): number { let used = 0; let i = 0; while (i < text.length) { - const ch = String.fromCodePoint(text.codePointAt(i)!); + const codePoint = text.codePointAt(i); + if (codePoint == null) break; + const ch = String.fromCodePoint(codePoint); const cw = stringWidth(ch); if (used + cw > width) return i; used += cw; @@ -65,7 +67,8 @@ export function sliceTailToWidth(text: string, width: number): string { let used = 0; let start = text.length; while (start > 0) { - const prev = text.codePointAt(start - 1)!; + const prev = text.codePointAt(start - 1); + if (prev == null) break; const step = prev >= 0xdc00 && prev <= 0xdfff && start >= 2 ? 2 : 1; const ch = text.slice(start - step, start); const cw = stringWidth(ch); @@ -104,7 +107,8 @@ function wrapNarrow(line: string, w: number): RowRange[] { const windowEnd = pos + w; let breakAt = -1; for (let i = windowEnd; i > pos; i--) { - if (/\s/.test(line[i]!)) { + const ch = line[i]; + if (ch != null && /\s/.test(ch)) { breakAt = i; break; } @@ -133,7 +137,9 @@ function wrapWide(line: string, w: number): RowRange[] { let i = 0; while (i < line.length) { - const ch = String.fromCodePoint(line.codePointAt(i)!); + const codePoint = line.codePointAt(i); + if (codePoint == null) break; + const ch = String.fromCodePoint(codePoint); const cw = stringWidth(ch); const isSpace = /\s/.test(ch); diff --git a/src/tui/view/lines.ts b/src/tui/view/lines.ts index 8f5d1ce0d..e0f15bdc2 100644 --- a/src/tui/view/lines.ts +++ b/src/tui/view/lines.ts @@ -140,20 +140,29 @@ export function viewToLines( widths.pop(); cols = cols.slice(0, widths.length); } - if (widths.length === 1 && widths[0]! > available) widths[0] = available; + const firstWidth = widths[0]; + if (widths.length === 1 && firstWidth != null && firstWidth > available) + widths[0] = available; const leftover = available - total(); - if (leftover > 0 && widths.length > 0) - widths[widths.length - 1] = widths[widths.length - 1]! + leftover; + if (leftover > 0 && widths.length > 0) { + const lastIndex = widths.length - 1; + const last = widths[lastIndex]; + if (last == null) throw new Error("grid column width missing"); + widths[lastIndex] = last + leftover; + } const lines: StyledLine[] = []; for (const r of allRows) { const cells = r.slice(0, widths.length); const segs: StyledLine = []; for (let i = 0; i < cells.length; i++) { - const cellNode = cells[i]!; - const cellLine = renderCell(cellNode, widths[i]!, palette); + const cellNode = cells[i]; + if (cellNode == null) throw new Error("grid cell missing"); + const colWidth = widths[i]; + if (colWidth == null) throw new Error("grid column width missing"); + const cellLine = renderCell(cellNode, colWidth, palette); const align = (cols[i]?.align ?? "left") as "left" | "right" | "center"; - const padded = padSegments(cellLine, widths[i]!, align); + const padded = padSegments(cellLine, colWidth, align); segs.push(...padded); if (i < cells.length - 1) segs.push({ text: " ".repeat(GAP) }); } diff --git a/src/tui/wave6.test.ts b/src/tui/wave6.test.ts index 354b95731..c1c0aeb5e 100644 --- a/src/tui/wave6.test.ts +++ b/src/tui/wave6.test.ts @@ -2,6 +2,7 @@ * Wave 6: command palette, long-log windowing, chrome zones, keyboard copy. */ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { IDLE_TRANSCRIPT_FLOOR } from "./geometry/index"; import { focusOwner, scrollLease } from "./focus/index"; import { withTestRenderer } from "./harness"; @@ -66,10 +67,10 @@ describe("Wave 6: command list", () => { expect(frame).not.toMatch(/│\s*>\s*│/); expect(frame).toContain("/compact"); // List labels live in overlayItems (frame may clip first row under tight height). - expect(shell.overlayItems[0]).toBe(CATALOG[0]!.label); + expect(shell.overlayItems[0]).toBe(defined(CATALOG[0]).label); moveOverlaySelection(shell, 1); - expect(shell.overlayList!.activeIndex).toBe(1); + expect(defined(shell.overlayList).activeIndex).toBe(1); closeInsetOverlay(shell); expect(shell.overlayList).toBeNull(); @@ -99,7 +100,9 @@ describe("Wave 6: command list", () => { const helpIdx = shell.paletteCommands.findIndex((c) => c.id === "help"); expect(helpIdx).toBeGreaterThanOrEqual(0); for (let i = 0; i < helpIdx; i++) moveOverlaySelection(shell, 1); - expect(shell.paletteCommands[shell.overlayList!.activeIndex]!.id).toBe("help"); + expect(defined(shell.paletteCommands[defined(shell.overlayList).activeIndex]).id).toBe( + "help", + ); acceptOverlaySelection(shell); expect(dispatched).toEqual(["help"]); @@ -420,7 +423,7 @@ describe("Wave 6: chrome zones", () => { const frame = h.captureCharFrame(); const agentLine = frame.split("\n").find((line) => line.includes("· 0:42 · grep")); expect(agentLine).toBeDefined(); - expect(stringWidth(agentLine!.trimEnd())).toBeLessThanOrEqual( + expect(stringWidth(defined(agentLine).trimEnd())).toBeLessThanOrEqual( shell.layout.sideMargin + shell.layout.contentWidth, ); expect(agentLine).toContain("…"); @@ -714,8 +717,8 @@ describe("CL-5741: chrome zone rows re-fit on terminal resize", () => { expect(taskLine).toBeDefined(); expect(agentLine).toBeDefined(); const maxPainted = shell.layout.sideMargin + shell.layout.contentWidth; - expect(stringWidth(taskLine!.trimEnd())).toBeLessThanOrEqual(maxPainted); - expect(stringWidth(agentLine!.trimEnd())).toBeLessThanOrEqual(maxPainted); + expect(stringWidth(defined(taskLine).trimEnd())).toBeLessThanOrEqual(maxPainted); + expect(stringWidth(defined(agentLine).trimEnd())).toBeLessThanOrEqual(maxPainted); expect(taskLine).toContain("[ ]"); expect(agentLine).toContain("· 0:42 · grep"); expect(narrowFrame).not.toContain(uniqueTaskPhrase); @@ -965,7 +968,7 @@ describe("reasoning effort flash TTL", () => { flashSchedule: (fn, ms) => { expect(ms).toBe(RUNTIME_FLASH_MS); lapse.push(fn); - return () => {}; + return () => undefined; }, }); try { diff --git a/src/tui/welcome.ts b/src/tui/welcome.ts index 6fee7aa01..f51975cb1 100644 --- a/src/tui/welcome.ts +++ b/src/tui/welcome.ts @@ -232,7 +232,7 @@ export async function runWelcome(config: WelcomeConfig = {}): Promise { fit(); paint(); - let resolveDone: (value: boolean) => void = () => {}; + let resolveDone: (value: boolean) => void = () => undefined; const done = new Promise((resolve) => { resolveDone = resolve; }); diff --git a/src/tui/workspace-watch.test.ts b/src/tui/workspace-watch.test.ts index 093ceb469..2508345e0 100644 --- a/src/tui/workspace-watch.test.ts +++ b/src/tui/workspace-watch.test.ts @@ -39,7 +39,7 @@ describe("watchGitBranch", () => { const clock = fakeClock(); const stop = watchGitBranch({ cwd: "/repo", - onBranch: () => {}, + onBranch: () => undefined, fetchBranch: () => { calls += 1; return new Promise((resolve) => { From 71c52bce93a84e92bdde648c34f27522152a2e2a Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 22:39:36 -0700 Subject: [PATCH 04/10] Replace subagent empty functions and non-null assertions --- src/subagent/agent-fleet.test.ts | 339 +++++++++--------- src/subagent/agent-fleet.ts | 8 +- src/subagent/ask-director.test.ts | 4 +- src/subagent/followup-live-agent.test.ts | 26 +- src/subagent/index.test.ts | 9 +- src/subagent/intervention-log.ts | 2 +- src/subagent/lifecycle-tools.test.ts | 73 ++-- src/subagent/nudge-director.test.ts | 9 +- src/subagent/nudge-director.ts | 2 +- src/subagent/report.ts | 11 +- src/subagent/retain-salvage.test.ts | 2 +- src/subagent/run-persist-close.test.ts | 17 +- .../run-resolved-provider-failure.test.ts | 8 +- src/subagent/run-settlement.test.ts | 21 +- src/subagent/run.ts | 11 +- src/subagent/session-store.test.ts | 78 ++-- src/subagent/session-store.ts | 2 +- src/subagent/shell-evidence.ts | 7 +- src/subagent/spawn-agent-worktree.test.ts | 27 +- src/subagent/tool-preview.test.ts | 5 +- src/subagent/trace-reader.test.ts | 13 +- src/subagent/trace-reader.ts | 3 +- src/subagent/worktree.test.ts | 3 +- 23 files changed, 358 insertions(+), 322 deletions(-) diff --git a/src/subagent/agent-fleet.test.ts b/src/subagent/agent-fleet.test.ts index 36eb52c79..ca244b3da 100644 --- a/src/subagent/agent-fleet.test.ts +++ b/src/subagent/agent-fleet.test.ts @@ -27,6 +27,7 @@ import { AGENTS_PANEL_LINGER_MS, formatAgentsPanel } from "../tui/chrome-state.j import { forcedStopReport } from "./stop-policy.js"; import type { RunSubAgentParams, RunSubAgentResult } from "./types.js"; import { INTERVENTION_FILE } from "./intervention-log.js"; +import { defined } from "../../tests/helpers/defined.js"; const testPermissionGate = createPermissionGate({ approvals: [], @@ -46,8 +47,8 @@ function deferred(): { resolve: (v: T) => void; reject: (e: unknown) => void; } { - let resolve!: (v: T) => void; - let reject!: (e: unknown) => void; + let resolve: (v: T) => void = () => undefined; + let reject: (e: unknown) => void = () => undefined; const promise = new Promise((res, rej) => { resolve = res; reject = rej; @@ -225,7 +226,7 @@ describe("spawn_agent + wait_agents", () => { let callIndex = 0; const deps = makeDeps(async () => { const i = callIndex++; - return gates[i]!.promise; + return defined(gates[i]).promise; }); const spawn = createSpawnAgentTool(deps); const wait = createWaitAgentsTool({ sessions: deps.sessions, fleetRecords: deps.fleetRecords }); @@ -237,21 +238,21 @@ describe("spawn_agent + wait_agents", () => { ); const ids = spawned.map((s) => s.agent_id as string); - gates[0]!.resolve({ report: "first report" }); + defined(gates[0]).resolve({ report: "first report" }); const waited = await callTool(wait, { targets: [ids[0]], timeout_ms: 5000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { agent_id: string; status: string; report?: string }[]; expect(results).toHaveLength(1); - expect(results[0]!.status).toBe("done"); - expect(results[0]!.report).toBe("first report"); + expect(defined(results[0]).status).toBe("done"); + expect(defined(results[0]).report).toBe("first report"); // The other two remain untouched and running. - expect(deps.sessions.get(ids[1]!)?.status).toBe("running"); - expect(deps.sessions.get(ids[2]!)?.status).toBe("running"); + expect(deps.sessions.get(defined(ids[1]))?.status).toBe("running"); + expect(deps.sessions.get(defined(ids[2]))?.status).toBe("running"); - gates[1]!.resolve({ report: "second" }); - gates[2]!.resolve({ report: "third" }); + defined(gates[1]).resolve({ report: "second" }); + defined(gates[2]).resolve({ report: "third" }); }); test("wait_agents times out on a still-running agent without cancelling it, and can be called again", async () => { @@ -270,7 +271,7 @@ describe("spawn_agent + wait_agents", () => { const first = await callTool(wait, { targets: [id], timeout_ms: 50 }); expect(first.timed_out).toBe(true); const firstResults = first.results as { agent_id: string; status: string }[]; - expect(firstResults[0]!.status).toBe("running"); + expect(defined(firstResults[0]).status).toBe("running"); // Not cancelled, not failed — still running. expect(deps.sessions.get(id)?.status).toBe("running"); @@ -284,28 +285,28 @@ describe("spawn_agent + wait_agents", () => { status: string; report?: string; }[]; - expect(secondResults[0]!.status).toBe("done"); - expect(secondResults[0]!.report).toBe("finished"); + expect(defined(secondResults[0]).status).toBe("done"); + expect(defined(secondResults[0]).report).toBe("finished"); }); test("wait_agents with no targets waits on all uncollected agents in this fleet", async () => { const gates = [deferred(), deferred()]; let callIndex = 0; - const deps = makeDeps(async () => gates[callIndex++]!.promise); + const deps = makeDeps(async () => defined(gates[callIndex++]).promise); const spawn = createSpawnAgentTool(deps); const wait = createWaitAgentsTool({ sessions: deps.sessions, fleetRecords: deps.fleetRecords }); await callTool(spawn, { description: "a", prompt: "do it", intent: "explore" }); await callTool(spawn, { description: "b", prompt: "do it", intent: "explore" }); - gates[0]!.resolve({ report: "a done" }); + defined(gates[0]).resolve({ report: "a done" }); const result = await callTool(wait, { timeout_ms: 5000 }); expect(result.timed_out).toBe(false); const results = result.results as { status: string }[]; expect(results).toHaveLength(2); expect(results.some((r) => r.status === "done")).toBe(true); - gates[1]!.resolve({ report: "b done" }); + defined(gates[1]).resolve({ report: "b done" }); }); test("reports survive well past the session store's display cap (20) until wait_agents collects them", async () => { @@ -343,7 +344,7 @@ describe("spawn_agent + wait_agents", () => { // 25 open retained sessions is under the default maxRetained (50), so // the earliest is still present and resumable — not evicted. - expect(deps.sessions.get(ids[0]!)).toBeDefined(); + expect(deps.sessions.get(defined(ids[0]))).toBeDefined(); // Every single one is retrievable through wait_agents too. const waited = await callTool(wait, { targets: ids, timeout_ms: 5000 }); @@ -395,11 +396,11 @@ describe("spawn_agent + wait_agents", () => { stop_reason?: string; }[]; expect(results).toHaveLength(1); - expect(results[0]!.status).toBe("interrupted"); - expect(results[0]!.report).toContain("## Summary"); - expect(results[0]!.report).toContain("## Findings"); - expect(results[0]!.report).toContain("gate.ts"); - expect(results[0]!.stop_reason).toBe("cancelled"); + expect(defined(results[0]).status).toBe("interrupted"); + expect(defined(results[0]).report).toContain("## Summary"); + expect(defined(results[0]).report).toContain("## Findings"); + expect(defined(results[0]).report).toContain("gate.ts"); + expect(defined(results[0]).stop_reason).toBe("cancelled"); // Strip stays cancelled — salvage is for wait_agents, not a resurrection. expect(deps.sessions.get(id)?.status).toBe("cancelled"); expect(deps.sessions.get(id)?.lifecycle.state).toBe("cancelled"); @@ -432,8 +433,8 @@ describe("spawn_agent + wait_agents", () => { const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { status: string; error?: string; report?: string }[]; - expect(results[0]!.status).toBe("interrupted"); - expect(results[0]!.error).toBeUndefined(); + expect(defined(results[0]).status).toBe("interrupted"); + expect(defined(results[0]).error).toBeUndefined(); expect(deps.sessions.get(id)?.status).toBe("cancelled"); }); @@ -458,10 +459,10 @@ describe("spawn_agent + wait_agents", () => { error?: string; stop_reason?: string; }[]; - expect(results[0]!.status).toBe("done"); - expect(results[0]!.stop_reason).toBe("incomplete-report"); - expect(results[0]!.report).toContain("narrated instead of writing a report envelope"); - expect(results[0]!.error).toBeUndefined(); + expect(defined(results[0]).status).toBe("done"); + expect(defined(results[0]).stop_reason).toBe("incomplete-report"); + expect(defined(results[0]).report).toContain("narrated instead of writing a report envelope"); + expect(defined(results[0]).error).toBeUndefined(); }); test("failed spawn_agent wait_agents returns error not report", async () => { @@ -484,10 +485,10 @@ describe("spawn_agent + wait_agents", () => { error?: string; stop_reason?: string; }[]; - expect(results[0]!.status).toBe("failed"); - expect(results[0]!.error).toContain("provider blew up"); - expect(results[0]!.report).toBeUndefined(); - expect(results[0]!.stop_reason).toBeUndefined(); + expect(defined(results[0]).status).toBe("failed"); + expect(defined(results[0]).error).toContain("provider blew up"); + expect(defined(results[0]).report).toBeUndefined(); + expect(defined(results[0]).stop_reason).toBeUndefined(); }); test("interrupt salvage wait_agents includes stop_reason interrupted", async () => { @@ -512,10 +513,10 @@ describe("spawn_agent + wait_agents", () => { error?: string; stop_reason?: string; }[]; - expect(results[0]!.status).toBe("interrupted"); - expect(results[0]!.stop_reason).toBe("interrupted"); - expect(results[0]!.report).toContain("interrupted before finishing"); - expect(results[0]!.error).toBeUndefined(); + expect(defined(results[0]).status).toBe("interrupted"); + expect(defined(results[0]).stop_reason).toBe("interrupted"); + expect(defined(results[0]).report).toContain("interrupted before finishing"); + expect(defined(results[0]).error).toBeUndefined(); }); }); @@ -523,7 +524,7 @@ describe("spawn_agent same-cwd concurrency", () => { test("two concurrent implement-intent spawn_agent calls against the same cwd both start", async () => { const gates = [deferred(), deferred()]; let callIndex = 0; - const deps = makeDeps(async () => gates[callIndex++]!.promise, { cwd: "/repo" }); + const deps = makeDeps(async () => defined(gates[callIndex++]).promise, { cwd: "/repo" }); const spawn = createSpawnAgentTool(deps); const first = await callTool(spawn, { @@ -542,15 +543,15 @@ describe("spawn_agent same-cwd concurrency", () => { expect(first.status).toBe("running"); expect(second.status).toBe("running"); - gates[0]!.resolve({ report: "one done" }); - gates[1]!.resolve({ report: "two done" }); + defined(gates[0]).resolve({ report: "one done" }); + defined(gates[1]).resolve({ report: "two done" }); }); test("two concurrent shared-cwd spawn_agent lanes log concurrent-lane-overlap", async () => { const dir = await mkdtemp(join(tmpdir(), "fleet-overlap-")); const gates = [deferred(), deferred()]; let callIndex = 0; - const deps = makeDeps(async () => gates[callIndex++]!.promise, { cwd: "/repo" }); + const deps = makeDeps(async () => defined(gates[callIndex++]).promise, { cwd: "/repo" }); deps.getWorkdirBase = () => dir; const spawn = createSpawnAgentTool(deps); @@ -584,8 +585,8 @@ describe("spawn_agent same-cwd concurrency", () => { expect(log).toContain("build one"); expect(log).toContain("build two"); - gates[0]!.resolve({ report: "one done" }); - gates[1]!.resolve({ report: "two done" }); + defined(gates[0]).resolve({ report: "one done" }); + defined(gates[1]).resolve({ report: "two done" }); }); }); @@ -635,7 +636,7 @@ describe("wait mailbox session tombstone and pin", () => { // The earliest spawned agent's payload should have been tombstoned — // never collected, so it was evicted once the cap was exceeded. - const waited = await callTool(wait, { targets: [ids[0]!], timeout_ms: 5000 }); + const waited = await callTool(wait, { targets: [defined(ids[0])], timeout_ms: 5000 }); const results = waited.results as { agent_id: string; status: string; @@ -643,10 +644,10 @@ describe("wait mailbox session tombstone and pin", () => { hint?: string; }[]; expect(results).toHaveLength(1); - expect(results[0]!.status).not.toBe("unknown"); - expect(["done", "failed"]).toContain(results[0]!.status); - expect(results[0]!.report).toBeUndefined(); - expect(results[0]!.hint).toContain("read_agent_trace"); + expect(defined(results[0]).status).not.toBe("unknown"); + expect(["done", "failed"]).toContain(defined(results[0]).status); + expect(defined(results[0]).report).toBeUndefined(); + expect(defined(results[0]).hint).toContain("read_agent_trace"); }); test("spawn_agent call.id reuse still pins the new session", async () => { @@ -688,7 +689,7 @@ describe("wait mailbox session tombstone and pin", () => { const waited = await callTool(wait, { targets: ["reuse-id"], timeout_ms: 1000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { status: string }[]; - expect(results[0]!.status).toBe("done"); + expect(defined(results[0]).status).toBe("done"); }); test("wait on a pruned mailbox member is tombstone not eternal running", () => { @@ -832,7 +833,7 @@ describe("wait_agents caller scope", () => { test("mode=all stays blocked until every target is terminal", async () => { const gates = [deferred(), deferred()]; let callIndex = 0; - const deps = makeDeps(async () => gates[callIndex++]!.promise); + const deps = makeDeps(async () => defined(gates[callIndex++]).promise); const spawn = createSpawnAgentTool(deps); const wait = createWaitAgentsTool({ sessions: deps.sessions, @@ -851,13 +852,13 @@ describe("wait_agents caller scope", () => { }); const ids = [first.agent_id as string, second.agent_id as string]; - gates[0]!.resolve({ report: "a done" }); + defined(gates[0]).resolve({ report: "a done" }); const partial = await callTool(wait, { targets: ids, mode: "all", timeout_ms: 50 }); expect(partial.timed_out).toBe(true); const partialResults = partial.results as { status: string }[]; expect(partialResults.some((r) => r.status === "running")).toBe(true); - gates[1]!.resolve({ report: "b done" }); + defined(gates[1]).resolve({ report: "b done" }); const finished = await callTool(wait, { targets: ids, mode: "all", timeout_ms: 5000 }); expect(finished.timed_out).toBe(false); const finishedResults = finished.results as { status: string }[]; @@ -869,12 +870,12 @@ describe("wait_agents caller scope", () => { let callIndex = 0; const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); - return gates[callIndex++]!.promise; + return defined(gates[callIndex++]).promise; }); const spawn = createSpawnAgentTool(deps); const wait = createWaitAgentsTool({ @@ -903,22 +904,22 @@ describe("wait_agents caller scope", () => { // sibling is still running. if (interrupt.kind !== "full") throw new Error("expected full tool"); await interrupt.handler( - { id: "int-1", name: "interrupt_agent", arguments: { target: ids[0]! } }, + { id: "int-1", name: "interrupt_agent", arguments: { target: defined(ids[0]) } }, new AbortController().signal, ); const partial = await callTool(wait, { targets: ids, mode: "all", timeout_ms: 50 }); expect(partial.timed_out).toBe(true); const partialResults = partial.results as { agent_id: string; status: string }[]; - expect(partialResults.find((r) => r.agent_id === ids[0]!)?.status).toBe("interrupted"); - expect(partialResults.find((r) => r.agent_id === ids[1]!)?.status).toBe("running"); + expect(partialResults.find((r) => r.agent_id === defined(ids[0]))?.status).toBe("interrupted"); + expect(partialResults.find((r) => r.agent_id === defined(ids[1]))?.status).toBe("running"); - gates[1]!.resolve({ report: "b done" }); + defined(gates[1]).resolve({ report: "b done" }); const finished = await callTool(wait, { targets: ids, mode: "all", timeout_ms: 5000 }); expect(finished.timed_out).toBe(false); const finishedResults = finished.results as { agent_id: string; status: string }[]; - expect(finishedResults.find((r) => r.agent_id === ids[0]!)?.status).toBe("interrupted"); - expect(finishedResults.find((r) => r.agent_id === ids[1]!)?.status).toBe("done"); + expect(finishedResults.find((r) => r.agent_id === defined(ids[0]))?.status).toBe("interrupted"); + expect(finishedResults.find((r) => r.agent_id === defined(ids[1]))?.status).toBe("done"); // Leave the interrupted gate unresolved — interrupt unblocked the wait // without the run settling. }); @@ -955,7 +956,7 @@ describe("wait_agents caller scope", () => { results: { status: string }[]; }; expect(parsed.timed_out).toBe(true); - expect(parsed.results[0]!.status).toBe("running"); + expect(defined(parsed.results[0]).status).toBe("running"); expect(deps.sessions.get(id)?.status).toBe("running"); gate.resolve({ report: "done" }); @@ -967,10 +968,10 @@ describe("interrupt_agent unblocks wait_agents", () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1035,18 +1036,18 @@ describe("interrupt_agent unblocks wait_agents", () => { const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { status: string; report?: string }[]; - expect(results[0]!.status).toBe("interrupted"); - expect(results[0]!.report).toContain("partial"); + expect(defined(results[0]).status).toBe("interrupted"); + expect(defined(results[0]).report).toContain("partial"); }); test("send_input soft-deliver does not complete wait_agents", async () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1069,7 +1070,7 @@ describe("interrupt_agent unblocks wait_agents", () => { const waited = await callTool(wait, { targets: [id], timeout_ms: 50 }); expect(waited.timed_out).toBe(true); const results = waited.results as { status: string }[]; - expect(results[0]!.status).toBe("running"); + expect(defined(results[0]).status).toBe("running"); gate.resolve({ report: "done" }); }); @@ -1078,10 +1079,10 @@ describe("interrupt_agent unblocks wait_agents", () => { const followupGate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => followupGate.promise, - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1112,8 +1113,8 @@ describe("interrupt_agent unblocks wait_agents", () => { report?: string; stop_reason?: string; }[]; - expect(results[0]!.status).toBe("done"); - expect(results[0]!.report).toBe("later"); + expect(defined(results[0]).status).toBe("done"); + expect(defined(results[0]).report).toBe("later"); }); test("CL-7331: send_input interrupt keeps wait live until the queued followup completes", async () => { @@ -1121,10 +1122,10 @@ describe("interrupt_agent unblocks wait_agents", () => { const followupGate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => followupGate.promise, - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1163,7 +1164,7 @@ describe("interrupt_agent unblocks wait_agents", () => { // immediate terminal interrupted), and list must agree with lifecycle. const pending = await callTool(wait, { targets: [id], timeout_ms: 50 }); expect(pending.timed_out).toBe(true); - expect((pending.results as { status: string }[])[0]!.status).toBe("running"); + expect(defined((pending.results as { status: string }[])[0]).status).toBe("running"); const listed = await callTool(list, {}); const entry = (listed.agents as { agent_id: string; status: string; lifecycle: string }[]).find( @@ -1191,8 +1192,8 @@ describe("interrupt_agent unblocks wait_agents", () => { const done = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(done.timed_out).toBe(false); const doneResults = done.results as { status: string; report?: string }[]; - expect(doneResults[0]!.status).toBe("done"); - expect(doneResults[0]!.report).toBe("followup report"); + expect(defined(doneResults[0]).status).toBe("done"); + expect(defined(doneResults[0]).report).toBe("followup report"); }); test("close_agent overlay survives a send_input followup completing in the close window", async () => { @@ -1202,9 +1203,9 @@ describe("interrupt_agent unblocks wait_agents", () => { const deps = makeDeps(async (params) => { params.onAgentReady?.({ close: async () => closeHold.promise, - interrupt: () => {}, + interrupt: () => undefined, followup: async () => followupGate.promise, - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1241,7 +1242,7 @@ describe("interrupt_agent unblocks wait_agents", () => { const waited = await waiting; expect(waited.timed_out).toBe(false); const results = waited.results as { status: string }[]; - expect(results[0]!.status).toBe("interrupted"); + expect(defined(results[0]).status).toBe("interrupted"); expect(deps.fleetRecords.peek(id)?.status).toBe("interrupted"); closeHold.resolve(undefined); @@ -1255,9 +1256,9 @@ describe("interrupt_agent unblocks wait_agents", () => { const deps = makeDeps(async (params) => { params.onAgentReady?.({ close: async () => closeHold.promise, - interrupt: () => {}, + interrupt: () => undefined, followup: async () => followupGate.promise, - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1321,7 +1322,7 @@ describe("interrupt_agent unblocks wait_agents", () => { const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { status: string }[]; - expect(results[0]!.status).toBe("interrupted"); + expect(defined(results[0]).status).toBe("interrupted"); closeHold.resolve(undefined); await closing; @@ -1351,10 +1352,10 @@ describe("interrupt_agent unblocks wait_agents", () => { const followupGate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => followupGate.promise, - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1385,8 +1386,8 @@ describe("interrupt_agent unblocks wait_agents", () => { const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { status: string; report?: string }[]; - expect(results[0]!.status).toBe("interrupted"); - expect(results[0]!.report).toContain("salvage"); + expect(defined(results[0]).status).toBe("interrupted"); + expect(defined(results[0]).report).toContain("salvage"); }); test("send_input interrupt queued overlay clears when the followup is admitted", async () => { @@ -1396,7 +1397,7 @@ describe("interrupt_agent unblocks wait_agents", () => { admission.enqueue({ id: "holder", provider: "p", - start: () => {}, + start: () => undefined, }); const worker = sessions.start({ description: "looping", @@ -1408,7 +1409,7 @@ describe("interrupt_agent unblocks wait_agents", () => { sessions.markRunning(worker.id); fleetRecords.register(worker.id); const followupGate = deferred(); - sessions.registerInterrupt(worker.id, () => {}); + sessions.registerInterrupt(worker.id, () => undefined); sessions.registerFollowup(worker.id, async () => followupGate.promise); const sendInput = createSendInputTool({ sessions, fleetRecords }); @@ -1425,7 +1426,7 @@ describe("interrupt_agent unblocks wait_agents", () => { const queuedWait = await callTool(wait, { targets: [worker.id], timeout_ms: 50 }); expect(queuedWait.timed_out).toBe(true); - expect((queuedWait.results as { status: string }[])[0]!.status).toBe("queued"); + expect(defined((queuedWait.results as { status: string }[])[0]).status).toBe("queued"); const queuedList = await callTool(list, {}); const queuedEntry = ( queuedList.agents as { agent_id: string; status: string; lifecycle: string }[] @@ -1439,7 +1440,7 @@ describe("interrupt_agent unblocks wait_agents", () => { expect(sessions.get(worker.id)?.lifecycleStatus).toBe("running"); const runningWait = await callTool(wait, { targets: [worker.id], timeout_ms: 50 }); expect(runningWait.timed_out).toBe(true); - expect((runningWait.results as { status: string }[])[0]!.status).toBe("running"); + expect(defined((runningWait.results as { status: string }[])[0]).status).toBe("running"); const runningList = await callTool(list, {}); const runningEntry = ( runningList.agents as { agent_id: string; status: string; lifecycle: string }[] @@ -1454,10 +1455,10 @@ describe("interrupt_agent unblocks wait_agents", () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1500,10 +1501,10 @@ describe("interrupt_agent unblocks wait_agents", () => { const settle = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return settle.promise; }); @@ -1534,8 +1535,8 @@ describe("interrupt_agent unblocks wait_agents", () => { ); const early = await callTool(wait, { targets: [id], timeout_ms: 5000 }); - expect((early.results as { status: string }[])[0]!.status).toBe("interrupted"); - expect((early.results as { report?: string }[])[0]!.report).toBeUndefined(); + expect(defined((early.results as { status: string }[])[0]).status).toBe("interrupted"); + expect(defined((early.results as { report?: string }[])[0]).report).toBeUndefined(); settle.resolve({ report: "## Summary\nStopped.\n## Findings\nsalvage\n## Blockers\ninterrupted\n## Paths\n", @@ -1545,8 +1546,8 @@ describe("interrupt_agent unblocks wait_agents", () => { const again = await callTool(wait, { targets: [id], timeout_ms: 5000 }); const results = again.results as { status: string; report?: string }[]; - expect(results[0]!.status).toBe("interrupted"); - expect(results[0]!.report).toContain("salvage"); + expect(defined(results[0]).status).toBe("interrupted"); + expect(defined(results[0]).report).toContain("salvage"); }); test("soft-interrupt wait collects so a later followup cannot resurrect done", async () => { @@ -1561,7 +1562,7 @@ describe("interrupt_agent unblocks wait_agents", () => { }); sessions.markRunning(worker.id); fleetRecords.register(worker.id); - sessions.registerInterrupt(worker.id, () => {}); + sessions.registerInterrupt(worker.id, () => undefined); sessions.interruptOne(worker.id); // Mirror interrupt_agent: soft interrupt alone projects as running while // in-flight, so the mailbox must flip for wait to see "interrupted". @@ -1572,7 +1573,7 @@ describe("interrupt_agent unblocks wait_agents", () => { const waited = await callTool(wait, { targets: [worker.id], timeout_ms: 1000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { status: string }[]; - expect(results[0]!.status).toBe("interrupted"); + expect(defined(results[0]).status).toBe("interrupted"); expect(fleetRecords.peek(worker.id)?.collected).toBe(true); fleetRecords.completeAfterInterrupt(worker.id, "resurrected reply"); @@ -1586,10 +1587,10 @@ describe("interrupt_agent unblocks wait_agents", () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => followupGate.promise, - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1614,8 +1615,8 @@ describe("interrupt_agent unblocks wait_agents", () => { const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(waited.timed_out).toBe(false); const results = waited.results as { status: string; report?: string }[]; - expect(results[0]!.status).toBe("done"); - expect(results[0]!.report).toBe("followup report"); + expect(defined(results[0]).status).toBe("done"); + expect(defined(results[0]).report).toBe("followup report"); }); }); @@ -1624,10 +1625,10 @@ describe("close_agent unblocks wait_agents", () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1701,12 +1702,12 @@ describe("list_agents", () => { }[]; }; expect(parsed.agents).toHaveLength(1); - expect(parsed.agents[0]!.agent_id).toBe(spawned.agent_id as string); - expect(parsed.agents[0]!.status).toBe("running"); - expect(parsed.agents[0]!.collected).toBe(false); - expect(parsed.agents[0]!.director).toBe("explorer"); - expect(parsed.agents[0]!.description).toBe("mine"); - expect(parsed.agents[0]!.lifecycle).toBe("pending_init"); + expect(defined(parsed.agents[0]).agent_id).toBe(spawned.agent_id as string); + expect(defined(parsed.agents[0]).status).toBe("running"); + expect(defined(parsed.agents[0]).collected).toBe(false); + expect(defined(parsed.agents[0]).director).toBe("explorer"); + expect(defined(parsed.agents[0]).description).toBe("mine"); + expect(defined(parsed.agents[0]).lifecycle).toBe("pending_init"); gate.resolve({ report: "done" }); }); @@ -1714,10 +1715,10 @@ describe("list_agents", () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1751,9 +1752,9 @@ describe("list_agents", () => { agents: { agent_id: string; status: string; stop_reason?: string }[]; }; expect(parsed.agents).toHaveLength(1); - expect(parsed.agents[0]!.agent_id).toBe(id); - expect(parsed.agents[0]!.status).toBe("interrupted"); - expect(parsed.agents[0]!.stop_reason).toBe("interrupted"); + expect(defined(parsed.agents[0]).agent_id).toBe(id); + expect(defined(parsed.agents[0]).status).toBe("interrupted"); + expect(defined(parsed.agents[0]).stop_reason).toBe("interrupted"); expect(list.definition.description).toContain("stop_reason"); gate.resolve({ report: "done" }); }); @@ -1762,17 +1763,17 @@ describe("list_agents", () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); void params.askDirectorPort ?.register({ question: "which file should I edit?", questionId: "ask-1", }) - .catch(() => {}); + .catch(() => undefined); return gate.promise; }); const spawn = createSpawnAgentTool(deps); @@ -1807,12 +1808,12 @@ describe("list_agents", () => { }[]; }; expect(parsed.agents).toHaveLength(1); - expect(parsed.agents[0]!.agent_id).toBe(spawned.agent_id as string); - expect(parsed.agents[0]!.status).toBe("awaiting_director"); - expect(parsed.agents[0]!.collected).toBe(false); - expect(parsed.agents[0]!.description).toBe("need a path"); - expect(parsed.agents[0]!.question).toBe("which file should I edit?"); - expect(parsed.agents[0]!.question_id).toBe("ask-1"); + expect(defined(parsed.agents[0]).agent_id).toBe(spawned.agent_id as string); + expect(defined(parsed.agents[0]).status).toBe("awaiting_director"); + expect(defined(parsed.agents[0]).collected).toBe(false); + expect(defined(parsed.agents[0]).description).toBe("need a path"); + expect(defined(parsed.agents[0]).question).toBe("which file should I edit?"); + expect(defined(parsed.agents[0]).question_id).toBe("ask-1"); expect(list.definition.description).toContain("question_id"); gate.resolve({ report: "done" }); }); @@ -1821,10 +1822,10 @@ describe("list_agents", () => { const gate = deferred(); const deps = makeDeps(async (params) => { params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return gate.promise; }); @@ -1846,11 +1847,11 @@ describe("list_agents", () => { new AbortController().signal, ); const agents = deps.sessions.list(); - const session = agents[0]!; + const session = defined(agents[0]); expect(session.status).toBe("running"); expect(session.lifecycleStatus).toBe("interrupted"); expect(agentLaneIsLive(session)).toBe(false); - const finishedAt = session.finishedAt!; + const finishedAt = defined(session.finishedAt); expect(finishedAt).toBeNumber(); const inside = finishedAt + 1_000; expect(fleetProgress(agents, inside).running).toBe(0); @@ -1918,11 +1919,15 @@ describe("spawn_agent dispatch contracts", () => { }); await new Promise((resolve) => setTimeout(resolve, 20)); expect(captured).toHaveLength(1); - expect(captured[0]!.orchestrator).toBe(true); - expect(captured[0]!.orchestratorTier).toBe("nested-orchestrator"); - expect(captured[0]!.tier).toBe("nested-orchestrator"); - expect(captured[0]!.nestedDispatch).toBeDefined(); - expect(captured[0]!.nestedDispatch?.spawnAllowlist).toEqual(["intern", "explorer", "critic"]); + expect(defined(captured[0]).orchestrator).toBe(true); + expect(defined(captured[0]).orchestratorTier).toBe("nested-orchestrator"); + expect(defined(captured[0]).tier).toBe("nested-orchestrator"); + expect(defined(captured[0]).nestedDispatch).toBeDefined(); + expect(defined(captured[0]).nestedDispatch?.spawnAllowlist).toEqual([ + "intern", + "explorer", + "critic", + ]); }); test("allowOrchestrator false strips nested spawn even for maySpawn directors", async () => { @@ -1939,9 +1944,9 @@ describe("spawn_agent dispatch contracts", () => { agent: "greybeard", }); await new Promise((resolve) => setTimeout(resolve, 20)); - expect(captured[0]!.orchestrator).toBeUndefined(); - expect(captured[0]!.nestedDispatch).toBeUndefined(); - expect(captured[0]!.tier).toBe("nested-orchestrator"); + expect(defined(captured[0]).orchestrator).toBeUndefined(); + expect(defined(captured[0]).nestedDispatch).toBeUndefined(); + expect(defined(captured[0]).tier).toBe("nested-orchestrator"); }); const FAIL_CLOSED_CRITERIA = @@ -2131,8 +2136,8 @@ describe("ask_director wait handshake", () => { deliver: (message: string) => void; } { return { - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", deliver: () => { throw new Error("soft send_input must not deliver while an ask is pending"); @@ -2170,7 +2175,7 @@ describe("ask_director wait handshake", () => { const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(waited.timed_out).toBe(false); - const first = (waited.results as Record[])[0]!; + const first = defined((waited.results as Record[])[0]); expect(first.status).toBe("awaiting_director"); expect(first.question).toBe("which file should I edit?"); expect(first.question_id).toBe("ask-1"); @@ -2179,7 +2184,7 @@ describe("ask_director wait handshake", () => { const rewait = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(rewait.timed_out).toBe(false); - const again = (rewait.results as Record[])[0]!; + const again = defined((rewait.results as Record[])[0]); expect(again.status).toBe("awaiting_director"); expect(again.question_id).toBe("ask-1"); @@ -2188,12 +2193,12 @@ describe("ask_director wait handshake", () => { const after = await callTool(wait, { targets: [id], timeout_ms: 50 }); expect(after.timed_out).toBe(true); - expect((after.results as { status: string }[])[0]!.status).toBe("running"); + expect(defined((after.results as { status: string }[])[0]).status).toBe("running"); gate.resolve({ report: "done" }); const done = await callTool(wait, { targets: [id], timeout_ms: 5000 }); expect(done.timed_out).toBe(false); - expect((done.results as { status: string }[])[0]!.status).toBe("done"); + expect(defined((done.results as { status: string }[])[0]).status).toBe("done"); }); test("mode=all unblocks on any ask", async () => { @@ -2203,10 +2208,10 @@ describe("ask_director wait handshake", () => { const deps = makeDeps(async (params) => { n += 1; params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); if (n === 1) { void params.askDirectorPort @@ -2265,8 +2270,8 @@ describe("ask_director wait handshake", () => { sessions.registerAsk("asking", { question: "which file?", questionId: "ask-1", - resolve: () => {}, - reject: () => {}, + resolve: () => undefined, + reject: () => undefined, }), ).toBe(true); sessions.start({ id: "extra", description: "e", agentId: "a", brief: "b" }); @@ -2290,8 +2295,8 @@ describe("ask_director wait handshake", () => { sessions.registerAsk("asking", { question: "which file?", questionId: "ask-1", - resolve: () => {}, - reject: () => {}, + resolve: () => undefined, + reject: () => undefined, }), ).toBe(true); expect(mailbox.peek("asking")?.status).toBe("awaiting_director"); @@ -2323,7 +2328,7 @@ describe("ask_director wait handshake", () => { }; process.on("unhandledRejection", onUnhandled); try { - expect(() => port!.register({ question: "late?", questionId: "ask-1" })).toThrow( + expect(() => defined(port).register({ question: "late?", questionId: "ask-1" })).toThrow( "could not register", ); await new Promise((resolve) => setTimeout(resolve, 0)); @@ -2377,7 +2382,7 @@ describe("admission queue", () => { expect(parsed.agents.filter((a) => a.status === "queued")).toHaveLength(18); expect(parsed.agents.filter((a) => a.status === "running")).toHaveLength(2); - const queuedId = results.find((r) => r.status === "queued")!.agent_id; + const queuedId = defined(results.find((r) => r.status === "queued")).agent_id; const wait = createWaitAgentsTool({ sessions: deps.sessions, fleetRecords: deps.fleetRecords, diff --git a/src/subagent/agent-fleet.ts b/src/subagent/agent-fleet.ts index 9ad39f5c5..6d5ef2f36 100644 --- a/src/subagent/agent-fleet.ts +++ b/src/subagent/agent-fleet.ts @@ -1158,13 +1158,13 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { ? { systemPromptRole: resolved.systemPromptRole } : {}), directorId: resolved.directorId, - ...(orchestrator + ...(orchestrator && nestedDispatch !== undefined ? { orchestrator: true, ...(resolved.orchestratorTier !== undefined ? { orchestratorTier: resolved.orchestratorTier } : {}), - nestedDispatch: nestedDispatch!, + nestedDispatch, } : {}), ...(deps.deadlineMs !== undefined ? { deadlineMs: deps.deadlineMs } : {}), @@ -1190,7 +1190,7 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { if (hold.resolve === undefined || hold.reject === undefined) { const err = new Error("ask_director could not register a pending question"); hold.reject?.(err); - void answer.catch(() => {}); + void answer.catch(() => undefined); throw err; } const ok = deps.sessions.registerAsk(session.id, { @@ -1202,7 +1202,7 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { if (!ok) { const err = new Error("ask_director could not register a pending question"); hold.reject(err); - void answer.catch(() => {}); + void answer.catch(() => undefined); throw err; } return answer; diff --git a/src/subagent/ask-director.test.ts b/src/subagent/ask-director.test.ts index 5333f6832..b60f92d25 100644 --- a/src/subagent/ask-director.test.ts +++ b/src/subagent/ask-director.test.ts @@ -91,7 +91,7 @@ describe("evaluateAskDirector", () => { registered.push(question); return "answer"; }, - cancel: () => {}, + cancel: () => undefined, }; const controller = new AbortController(); controller.abort(); @@ -180,7 +180,7 @@ describe("evaluateAskDirector", () => { register: (): Promise => { throw new Error("ask_director could not register a pending question"); }, - cancel: () => {}, + cancel: () => undefined, }; const message = await handleAskDirector({ question: "which file?", diff --git a/src/subagent/followup-live-agent.test.ts b/src/subagent/followup-live-agent.test.ts index 3ea308c0e..3a1d6db75 100644 --- a/src/subagent/followup-live-agent.test.ts +++ b/src/subagent/followup-live-agent.test.ts @@ -2,7 +2,7 @@ * Regression guard: lifecycle-tools.test.ts proves interrupt_agent / * resume_agent behave correctly against *fake registered closures* at the * tool/store layer — it never exercises run.ts's real wiring, where - * `followup` calls `agent!.send()` on the same live agent object created by + * `followup` calls `agent.send()` on the same live agent object created by * `createAgentWithLiveToolDispatch`. A future refactor could make * `resume_agent` rebuild the agent instead of reusing it (exactly the * regression this feature exists to prevent — a rebuilt agent means the @@ -22,6 +22,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { withMockedModuleDuring } from "../../tests/helpers/mock-module.js"; +import { defined } from "../../tests/helpers/defined.js"; import { createPermissionGate } from "../permission/gate.js"; import type { RunSubAgentParams } from "./types.js"; @@ -75,17 +76,20 @@ function createStubAgent(opts?: { hangFromSend?: number }) { "abort", () => { if (timer !== undefined) clearTimeout(timer); - abort(optsSend.signal!.reason); + abort(defined(optsSend.signal).reason); }, { once: true }, ); }); }, - stream: () => (async function* () {})(), - deliver: () => {}, - close: async () => {}, - setSource: () => {}, - setSources: () => {}, + stream: () => + (async function* () { + yield* []; + })(), + deliver: () => undefined, + close: async () => undefined, + setSource: () => undefined, + setSources: () => undefined, history: async () => [], checkpoints: async () => [], readAt: async () => [], @@ -163,9 +167,11 @@ describe("interrupt_agent / resume_agent reuse the same live agent", () => { // the original turn's prompt and the followup message, proving the // followup was sent into the same live object rather than a fresh one // with empty history. - expect(capturedAgent!.sendLog.length).toBe(2); - expect(capturedAgent!.sendLog[0]).toContain("explore the codebase for the bug"); - expect(capturedAgent!.sendLog[1]).toBe("do X instead, not what the original prompt said"); + expect(defined(capturedAgent).sendLog.length).toBe(2); + expect(defined(capturedAgent).sendLog[0]).toContain("explore the codebase for the bug"); + expect(defined(capturedAgent).sendLog[1]).toBe( + "do X instead, not what the original prompt said", + ); expect(outcome.reply).toBe("reply #2"); }); diff --git a/src/subagent/index.test.ts b/src/subagent/index.test.ts index 8c4603e1c..57f6594e6 100644 --- a/src/subagent/index.test.ts +++ b/src/subagent/index.test.ts @@ -31,6 +31,7 @@ import type { ReactorInboundEvent, ReactorState, } from "@intx/types/runtime"; +import { defined } from "../../tests/helpers/defined.js"; describe("sub-agent teardown", () => { test("disposeSubAgentSession closes agent, awaits stream, and disposes posix tools once", async () => { @@ -74,7 +75,7 @@ describe("sub-agent teardown", () => { test("disposeSubAgentSession reaps posix tools before waiting on agent.close", async () => { const order: string[] = []; - let releaseClose!: () => void; + let releaseClose: () => void = () => undefined; const closeGate = new Promise((resolve) => { releaseClose = resolve; }); @@ -125,7 +126,7 @@ describe("sub-agent teardown", () => { agent: { close: () => { closeStarted = true; - return new Promise(() => {}); + return new Promise(() => undefined); }, }, posixTools, @@ -149,8 +150,8 @@ describe("sub-agent teardown", () => { test("spawn registry tracks in-flight plugin tool calls", async () => { const { plugin, snapshot } = createSubAgentSpawnRegistryPlugin(); expect(plugin.middleware).toBeDefined(); - const middleware = plugin.middleware!; - let release!: () => void; + const middleware = defined(plugin.middleware, "middleware"); + let release: () => void = () => undefined; const gate = new Promise((resolve) => { release = resolve; }); diff --git a/src/subagent/intervention-log.ts b/src/subagent/intervention-log.ts index 1eaa750c5..42e26e969 100644 --- a/src/subagent/intervention-log.ts +++ b/src/subagent/intervention-log.ts @@ -104,7 +104,7 @@ export type InterventionSink = ( ) => void; /** Sink that drops everything — the default, so logging is never required. */ -export const NOOP_INTERVENTION_SINK: InterventionSink = () => {}; +export const NOOP_INTERVENTION_SINK: InterventionSink = () => undefined; /** * Append-only sink over `/interventions.jsonl`. diff --git a/src/subagent/lifecycle-tools.test.ts b/src/subagent/lifecycle-tools.test.ts index 57f2d4a8a..0a663db17 100644 --- a/src/subagent/lifecycle-tools.test.ts +++ b/src/subagent/lifecycle-tools.test.ts @@ -10,6 +10,7 @@ import { import { createFleetMailbox, createWaitAgentsTool } from "./agent-fleet.js"; import { createSubAgentSessionStore, DEFAULT_MAX_ENTRY_CHARS } from "./session-store.js"; import { createAdmissionQueue } from "./admission.js"; +import { defined } from "../../tests/helpers/defined.js"; async function callTool( tool: @@ -80,8 +81,8 @@ describe("close_agent", () => { brief: "b", parentSessionId: parent.id, }); - sessions.registerClose(wedgedChild.id, () => new Promise(() => {})); - sessions.registerClose(parent.id, async () => {}); + sessions.registerClose(wedgedChild.id, () => new Promise(() => undefined)); + sessions.registerClose(parent.id, async () => undefined); // Exercise the store directly with a short deadline (the tool itself // uses the real ~30s bound, which would make this test slow). @@ -138,7 +139,7 @@ describe("resume_agent", () => { const fleetRecords = createFleetMailbox(sessions); const retained = sessions.start({ description: "d", agentId: "a", brief: "b", retained: true }); const history: string[] = ["first task"]; - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; sessions.registerFollowup( retained.id, (message: string) => @@ -162,8 +163,8 @@ describe("resume_agent", () => { expect(sessions.get(retained.id)?.lifecycleStatus).toBe("running"); expect(history).toEqual(["first task", "now do task two"]); - sessions.registerDeliver(retained.id, () => {}); - sessions.registerInterrupt(retained.id, () => {}); + sessions.registerDeliver(retained.id, () => undefined); + sessions.registerInterrupt(retained.id, () => undefined); const sendInput = createSendInputTool({ sessions }); const steered = await callTool(sendInput, { target: retained.id, message: "steer" }); expect(steered).toEqual({ agent_id: retained.id, status: "running" }); @@ -252,7 +253,7 @@ describe("resume_agent", () => { brief: "b", retained: true, }); - sessions.registerClose(closed.id, async () => {}); + sessions.registerClose(closed.id, async () => undefined); sessions.registerFollowup(closed.id, async () => "should not run"); sessions.complete(closed.id, "## Summary\nDone."); const closeAgent = createCloseAgentTool({ sessions, fleetRecords }); @@ -273,7 +274,7 @@ describe("resume_agent", () => { brief: "b", retained: true, }); - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; sessions.registerFollowup( worker.id, () => @@ -346,8 +347,8 @@ describe("resume_agent", () => { sessions.registerAsk(worker.id, { question: "which file?", questionId: "ask-1", - resolve: () => {}, - reject: () => {}, + resolve: () => undefined, + reject: () => undefined, }), ).toBe(true); @@ -375,7 +376,7 @@ describe("resume_agent", () => { brief: "b", retained: true, }); - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; sessions.registerFollowup( worker.id, () => @@ -392,8 +393,8 @@ describe("resume_agent", () => { const firstWait = await callTool(wait, { targets: [worker.id], timeout_ms: 1000 }); expect(firstWait.timed_out).toBe(false); const firstResults = firstWait.results as { status: string; report?: string }[]; - expect(firstResults[0]!.status).toBe("done"); - expect(firstResults[0]!.report).toBe("first report"); + expect(defined(firstResults[0]).status).toBe("done"); + expect(defined(firstResults[0]).report).toBe("first report"); const started = Date.now(); const resumed = await callTool(resumeAgent, { target: worker.id, message: "second turn" }); @@ -405,8 +406,8 @@ describe("resume_agent", () => { const collected = await waiting; expect(collected.timed_out).toBe(false); const results = collected.results as { status: string; report?: string }[]; - expect(results[0]!.status).toBe("done"); - expect(results[0]!.report).toBe("second report"); + expect(defined(results[0]).status).toBe("done"); + expect(defined(results[0]).report).toBe("second report"); }); test("interrupt then successful resume wait is done without leftover interrupted stop_reason", async () => { @@ -419,8 +420,8 @@ describe("resume_agent", () => { retained: true, }); sessions.markRunning(worker.id); - sessions.registerInterrupt(worker.id, () => {}); - let finish: (reply: string) => void = () => {}; + sessions.registerInterrupt(worker.id, () => undefined); + let finish: (reply: string) => void = () => undefined; sessions.registerFollowup( worker.id, () => @@ -442,8 +443,8 @@ describe("resume_agent", () => { status: string; stop_reason?: string; }[]; - expect(interruptedResults[0]!.status).toBe("interrupted"); - expect(interruptedResults[0]!.stop_reason).toBe("interrupted"); + expect(defined(interruptedResults[0]).status).toBe("interrupted"); + expect(defined(interruptedResults[0]).stop_reason).toBe("interrupted"); const resumed = await callTool(resumeAgent, { target: worker.id, message: "continue" }); expect(resumed.status).toBe("running"); @@ -457,9 +458,9 @@ describe("resume_agent", () => { report?: string; stop_reason?: string; }[]; - expect(results[0]!.status).toBe("done"); - expect(results[0]!.report).toBe("resumed report"); - expect(results[0]!.stop_reason).toBeUndefined(); + expect(defined(results[0]).status).toBe("done"); + expect(defined(results[0]).report).toBe("resumed report"); + expect(defined(results[0]).stop_reason).toBeUndefined(); }); test("resume followup rejection invokes close; close_agent tears down leftover", async () => { @@ -644,8 +645,8 @@ describe("send_input", () => { retained: true, }); sessions.markRunning(worker.id); - sessions.registerInterrupt(worker.id, () => {}); - let finish: (reply: string) => void = () => {}; + sessions.registerInterrupt(worker.id, () => undefined); + let finish: (reply: string) => void = () => undefined; sessions.registerFollowup( worker.id, () => @@ -673,9 +674,9 @@ describe("send_input", () => { report?: string; stop_reason?: string; }[]; - expect(results[0]!.status).toBe("done"); - expect(results[0]!.report).toBe("followup report"); - expect(results[0]!.stop_reason).toBeUndefined(); + expect(defined(results[0]).status).toBe("done"); + expect(defined(results[0]).report).toBe("followup report"); + expect(defined(results[0]).stop_reason).toBeUndefined(); }); test("followup throw after interrupt wait still has stop_reason interrupted", async () => { @@ -688,7 +689,7 @@ describe("send_input", () => { retained: true, }); sessions.markRunning(worker.id); - sessions.registerInterrupt(worker.id, () => {}); + sessions.registerInterrupt(worker.id, () => undefined); sessions.registerFollowup(worker.id, async () => { throw new Error("send failed"); }); @@ -705,8 +706,8 @@ describe("send_input", () => { status: string; stop_reason?: string; }[]; - expect(results[0]!.status).toBe("interrupted"); - expect(results[0]!.stop_reason).toBe("interrupted"); + expect(defined(results[0]).status).toBe("interrupted"); + expect(defined(results[0]).stop_reason).toBe("interrupted"); }); test("soft-delivers without flipping lifecycle or awaiting a reply", async () => { @@ -777,7 +778,7 @@ describe("send_input", () => { retained: true, }); sessions.markRunning(missing.id); - sessions.registerInterrupt(missing.id, () => {}); + sessions.registerInterrupt(missing.id, () => undefined); if (sendInput.kind !== "full") throw new Error("expected full tool"); const denied = await sendInput.handler( { @@ -821,7 +822,7 @@ describe("send_input", () => { retained: true, }); sessions.markRunning(interrupted.id); - sessions.registerInterrupt(interrupted.id, () => {}); + sessions.registerInterrupt(interrupted.id, () => undefined); sessions.registerDeliver(interrupted.id, () => { throw new Error("must not deliver to an interrupted session"); }); @@ -845,7 +846,7 @@ describe("send_input", () => { retained: true, }); sessions.markRunning(closed.id); - sessions.registerClose(closed.id, async () => {}); + sessions.registerClose(closed.id, async () => undefined); sessions.registerDeliver(closed.id, () => { throw new Error("must not deliver to a closed session"); }); @@ -881,7 +882,7 @@ describe("send_input", () => { }); for (const session of [nested, child, sibling]) { sessions.markRunning(session.id); - sessions.registerDeliver(session.id, () => {}); + sessions.registerDeliver(session.id, () => undefined); } const sendInput = createSendInputTool({ sessions, @@ -912,7 +913,7 @@ describe("send_input", () => { brief: "b", }); sessions.markRunning(worker.id); - sessions.registerDeliver(worker.id, () => {}); + sessions.registerDeliver(worker.id, () => undefined); const sendInput = createSendInputTool({ sessions, authority: { @@ -953,7 +954,7 @@ describe("nested lifecycle authority", () => { const sibling = sessions.start({ id: "sibling", description: "s", agentId: "a", brief: "b" }); for (const s of [child, sibling]) { sessions.markRunning(s.id); - sessions.registerInterrupt(s.id, () => {}); + sessions.registerInterrupt(s.id, () => undefined); } const interrupt = createInterruptAgentTool({ sessions, @@ -980,7 +981,7 @@ describe("nested lifecycle authority", () => { parentSessionId: nested.id, }); const sibling = sessions.start({ id: "sibling", description: "s", agentId: "a", brief: "b" }); - for (const s of [child, sibling]) sessions.registerClose(s.id, async () => {}); + for (const s of [child, sibling]) sessions.registerClose(s.id, async () => undefined); const close = createCloseAgentTool({ sessions, fleetRecords: createFleetMailbox(sessions), diff --git a/src/subagent/nudge-director.test.ts b/src/subagent/nudge-director.test.ts index 6451144ee..0c0364310 100644 --- a/src/subagent/nudge-director.test.ts +++ b/src/subagent/nudge-director.test.ts @@ -9,6 +9,7 @@ import { createCorbitsRetryPolicy } from "../agent/retry-policy.js"; import { COMPACTOR_KEEP_RECENT_TURNS, compactorNoOpFloor } from "../session/compactor.js"; import { SubAgentDirector } from "./nudge-director.js"; import type { AdmissionQueue } from "./admission.js"; +import { defined } from "../../tests/helpers/defined.js"; const state = { turns: [] } as unknown as ReactorState; const longState = { @@ -564,12 +565,12 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => { function stubAdmission(notes: { provider: string; until: number }[]): AdmissionQueue { return { enqueue: () => "running", - release: () => {}, - setCapacity: () => {}, + release: () => undefined, + setCapacity: () => undefined, notePressure: (provider, untilMs) => { notes.push({ provider, until: untilMs }); }, - cancel: () => {}, + cancel: () => undefined, occupied: () => false, }; } @@ -606,7 +607,7 @@ describe("SubAgentDirector infer retryPolicy", () => { }, }); expect(notes).toHaveLength(1); - expect(notes[0]!.provider).toBe("xai/thegreataxios"); + expect(defined(notes[0]).provider).toBe("xai/thegreataxios"); notes.length = 0; await stamped({ diff --git a/src/subagent/nudge-director.ts b/src/subagent/nudge-director.ts index a0eabec19..38019298a 100644 --- a/src/subagent/nudge-director.ts +++ b/src/subagent/nudge-director.ts @@ -141,7 +141,7 @@ export class SubAgentDirector extends DefaultDirector { // Structured stop-reason side channel: fired synchronously whenever this // director force-stops, so the caller learns the reason as a typed value // rather than re-parsing the forcedStopReport prose it returns. - private onForcedStop: (reason: ForcedStopReason) => void = () => {}; + private onForcedStop: (reason: ForcedStopReason) => void = () => undefined; /** Route this leaf's stop/nudge decisions to an intervention log. */ observeInterventions(sink: InterventionSink): void { diff --git a/src/subagent/report.ts b/src/subagent/report.ts index c7b0948a7..59a8d236d 100644 --- a/src/subagent/report.ts +++ b/src/subagent/report.ts @@ -139,12 +139,13 @@ export function parseSubAgentReport(reply: string): SubAgentReport { paths: "", }; } - for (let i = 0; i < matches.length; i++) { - const m = matches[i]!; - const name = m[1]!.toLowerCase(); + for (const [i, m] of matches.entries()) { + const name = m[1]; + if (name === undefined) continue; const start = (m.index ?? 0) + m[0].length; - const end = i + 1 < matches.length ? (matches[i + 1]!.index ?? text.length) : text.length; - sections[name] = text.slice(start, end).trim(); + const next = matches[i + 1]; + const end = next?.index ?? text.length; + sections[name.toLowerCase()] = text.slice(start, end).trim(); } return { summary: sections.summary ?? "", diff --git a/src/subagent/retain-salvage.test.ts b/src/subagent/retain-salvage.test.ts index fc541779a..6cd6d4cdb 100644 --- a/src/subagent/retain-salvage.test.ts +++ b/src/subagent/retain-salvage.test.ts @@ -56,7 +56,7 @@ describe("retained session lifecycle", () => { brief: "b", retained: true, }); - store.registerClose(s.id, async () => {}); + store.registerClose(s.id, async () => undefined); store.complete(s.id, "done"); } expect(store.list().length).toBeLessThanOrEqual(3); diff --git a/src/subagent/run-persist-close.test.ts b/src/subagent/run-persist-close.test.ts index 8816fbf96..db62514a3 100644 --- a/src/subagent/run-persist-close.test.ts +++ b/src/subagent/run-persist-close.test.ts @@ -30,11 +30,14 @@ function stubAgent() { turn: { role: "assistant", content: [] }, }; }, - stream: () => (async function* (): AsyncGenerator {})(), - deliver: () => {}, - close: async () => {}, - setSource: () => {}, - setSources: () => {}, + stream: () => + (async function* (): AsyncGenerator { + yield* []; + })(), + deliver: () => undefined, + close: async () => undefined, + setSource: () => undefined, + setSources: () => undefined, history: async () => [], checkpoints: async () => [], readAt: async () => [], @@ -118,7 +121,7 @@ describe("persist close_agent leftover dispose", () => { createAgentWithLiveToolDispatch: async () => ({ ...stubAgent(), - close: () => new Promise(() => {}), + close: () => new Promise(() => undefined), }) as unknown as Awaited>, }), async () => { @@ -175,7 +178,7 @@ describe("persist close_agent leftover dispose", () => { ...stubAgent(), close: () => { closeStarted = true; - return new Promise(() => {}); + return new Promise(() => undefined); }, }) as unknown as Awaited>, }), diff --git a/src/subagent/run-resolved-provider-failure.test.ts b/src/subagent/run-resolved-provider-failure.test.ts index 08572f187..caca1b10c 100644 --- a/src/subagent/run-resolved-provider-failure.test.ts +++ b/src/subagent/run-resolved-provider-failure.test.ts @@ -84,10 +84,10 @@ async function withResolvedProviderRun( data: { content: RAW_DIAGNOSTIC }, } as unknown as ReactorEmittedEvent; })(), - deliver: () => {}, - close: async () => {}, - setSource: () => {}, - setSources: () => {}, + deliver: () => undefined, + close: async () => undefined, + setSource: () => undefined, + setSources: () => undefined, history: async () => [], checkpoints: async () => [], readAt: async () => [], diff --git a/src/subagent/run-settlement.test.ts b/src/subagent/run-settlement.test.ts index 728b86ddb..b62d7e5fc 100644 --- a/src/subagent/run-settlement.test.ts +++ b/src/subagent/run-settlement.test.ts @@ -70,10 +70,10 @@ test("rejected workers settle prior rollups with the latest observed model", asy data: { model: "terminal-model" }, }; })(), - deliver: () => {}, - close: async () => {}, - setSource: () => {}, - setSources: () => {}, + deliver: () => undefined, + close: async () => undefined, + setSource: () => undefined, + setSources: () => undefined, history: async () => [], checkpoints: async () => [], readAt: async () => [], @@ -137,11 +137,14 @@ test("pre-progress cancellation settles as cancelled without changing rejection" send: async () => { throw new Error("send must not start after cancellation"); }, - stream: () => (async function* () {})(), - deliver: () => {}, - close: async () => {}, - setSource: () => {}, - setSources: () => {}, + stream: () => + (async function* () { + yield* []; + })(), + deliver: () => undefined, + close: async () => undefined, + setSource: () => undefined, + setSources: () => undefined, history: async () => [], checkpoints: async () => [], readAt: async () => [], diff --git a/src/subagent/run.ts b/src/subagent/run.ts index 426660387..36d11cb32 100644 --- a/src/subagent/run.ts +++ b/src/subagent/run.ts @@ -602,13 +602,16 @@ async function runSubAgentInner( // Typed reporting channel, Tier 3 leaves only. Gated by the existing // tier machinery — never invent a parallel check. if (params.tier === "leaf") { + if (turnToken === undefined) { + throw new Error("leaf dispatch is missing a turn token"); + } tools = [ ...tools, stringTool({ definition: submitResultDefinition, handler: async (rawArgs: Record): Promise => { const outcome = evaluateSubmitResult({ - turnToken: turnToken!, + turnToken, submittedToken: rawArgs.turn_token, result: rawArgs.result, ...(params.reportType !== undefined ? { outputType: params.reportType } : {}), @@ -1071,7 +1074,8 @@ async function runSubAgentInner( ): ReturnType["send"]> => { const pending = runSettlement.beginSend(); try { - const result = await agent!.send(message, options); + if (agent === null) throw new Error("sub-agent is not running"); + const result = await agent.send(message, options); await pending.settled; return result; } catch (error) { @@ -1167,7 +1171,8 @@ async function runSubAgentInner( : "Sub-agent finished without a textual result."; }; const deliver = (message: string): void => { - agent!.deliver({ + if (agent === null) throw new Error("sub-agent is not running"); + agent.deliver({ ref: { uid: 1, mailbox: "INBOX" }, headers: { from: "parent@local", diff --git a/src/subagent/session-store.test.ts b/src/subagent/session-store.test.ts index 9facbbf13..6a2be002b 100644 --- a/src/subagent/session-store.test.ts +++ b/src/subagent/session-store.test.ts @@ -8,6 +8,8 @@ import { formatAgentsPanel } from "../tui/chrome-state.js"; import type { ReactorEmittedEvent } from "@intx/inference"; +import { defined } from "../../tests/helpers/defined.js"; + function startCall(seq: number, callId: string, name: string) { return { type: "inference.tool_call.start" as const, @@ -304,7 +306,7 @@ describe("terminal stop reasons", () => { const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b" }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); expect(store.interruptOne(session.id).ok).toBe(true); expect(store.get(session.id)?.stopReason).toBe("interrupted"); }); @@ -313,8 +315,8 @@ describe("terminal stop reasons", () => { const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); - store.registerFollowup(session.id, () => new Promise(() => {})); + store.registerInterrupt(session.id, () => undefined); + store.registerFollowup(session.id, () => new Promise(() => undefined)); expect(store.sendInputOne(session.id, "stop that", { interrupt: true })).toEqual({ ok: true, status: "interrupted", @@ -359,7 +361,7 @@ describe("CL-6943 reusable worker sessions", () => { retained: true, provider: "p", }); - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; store.registerFollowup( session.id, () => @@ -392,7 +394,7 @@ describe("CL-6943 reusable worker sessions", () => { provider: "p", }); let followupStarted = false; - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; store.registerFollowup( session.id, () => @@ -406,7 +408,7 @@ describe("CL-6943 reusable worker sessions", () => { admission.enqueue({ id: "worker", provider: "p", - start: () => {}, + start: () => undefined, }), ).toBe("running"); expect(admission.occupied("worker")).toBe(true); @@ -418,7 +420,7 @@ describe("CL-6943 reusable worker sessions", () => { }); test("resume-from-completed is a live turn: send_input, interrupt, and appendEvent work", async () => { - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); store.markRunning(session.id); @@ -426,7 +428,7 @@ describe("CL-6943 reusable worker sessions", () => { store.registerDeliver(session.id, (message) => { delivered.push(message); }); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); store.registerFollowup( session.id, () => @@ -473,7 +475,7 @@ describe("CL-6943 reusable worker sessions", () => { const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); store.registerFollowup(session.id, async () => { throw new Error("send failed"); }); @@ -494,7 +496,7 @@ describe("CL-6943 reusable worker sessions", () => { const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); store.registerFollowup(session.id, async () => { throw new Error("send failed"); }); @@ -509,11 +511,11 @@ describe("CL-6943 reusable worker sessions", () => { }); test("interrupt then abort does not overwrite interrupted stamp to completed", async () => { - let rejectFollowup: (err: unknown) => void = () => {}; + let rejectFollowup: (err: unknown) => void = () => undefined; const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); store.registerFollowup( session.id, () => @@ -612,7 +614,7 @@ describe("CL-6943 reusable worker sessions", () => { test("closeOne fails a hung close instead of reporting shutdown success", async () => { const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); - store.registerClose(session.id, () => new Promise(() => {})); // never resolves + store.registerClose(session.id, () => new Promise(() => undefined)); // never resolves const started = Date.now(); await expect(store.closeOne(session.id, 25)).rejects.toThrow(/session close exceeded 25ms/); @@ -653,7 +655,7 @@ describe("CL-6943 reusable worker sessions", () => { const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); // registerClose always fires in production before onAgentReady's window // closes (CL-7001) — closeOne otherwise waits for it up to the deadline. - store.registerClose(session.id, async () => {}); + store.registerClose(session.id, async () => undefined); store.complete(session.id, "## Summary\nDone."); await store.closeOne(session.id, 1000); expect(store.resumeOne(session.id, "more")).toEqual({ ok: false, status: "shutdown" }); @@ -691,7 +693,7 @@ describe("CL-6943 reusable worker sessions", () => { brief: "b", retained: true, }); - store.registerClose(s.id, async () => {}); + store.registerClose(s.id, async () => undefined); store.complete(s.id, "## Summary\nDone."); } @@ -723,7 +725,7 @@ describe("CL-6943 reusable worker sessions", () => { brief: "b", retained: true, }); - store.registerClose(retained.id, async () => {}); + store.registerClose(retained.id, async () => undefined); store.complete(retained.id, "## Summary\nDone."); for (let i = 0; i < 3; i++) { @@ -733,7 +735,7 @@ describe("CL-6943 reusable worker sessions", () => { brief: "b", retained: true, }); - store.registerClose(s.id, async () => {}); + store.registerClose(s.id, async () => undefined); store.complete(s.id, "## Summary\nDone."); } @@ -755,8 +757,8 @@ describe("CL-6943 reusable worker sessions", () => { }); store.markRunning(running.id); // Resume it back to "running" so it is an open, actively-driven session. - store.registerClose(running.id, async () => {}); - store.registerFollowup(running.id, () => new Promise(() => {})); + store.registerClose(running.id, async () => undefined); + store.registerFollowup(running.id, () => new Promise(() => undefined)); store.complete(running.id, "## Summary\nDone."); store.resumeOne(running.id, "keep going"); expect(store.get(running.id)?.lifecycleStatus).toBe("running"); @@ -768,7 +770,7 @@ describe("CL-6943 reusable worker sessions", () => { brief: "b", retained: true, }); - store.registerClose(s.id, async () => {}); + store.registerClose(s.id, async () => undefined); store.complete(s.id, "## Summary\nDone."); } @@ -785,7 +787,7 @@ describe("CL-6943 reusable worker sessions", () => { brief: "b", retained: true, }); - store.registerClose(s.id, async () => {}); + store.registerClose(s.id, async () => undefined); store.complete(s.id, "## Summary\nDone."); } const openRetained = store @@ -802,7 +804,7 @@ describe("CL-6943 reusable worker sessions", () => { brief: "b", retained: true, }); - store.registerClose(retained.id, async () => {}); + store.registerClose(retained.id, async () => undefined); store.complete(retained.id, "## Summary\nDone."); await store.closeOne(retained.id, 1000); @@ -831,7 +833,7 @@ describe("interrupt stamps finishedAt once", () => { }); store.markRunning(session.id); store.appendEvent(session.id, startCall(1, "call-1", "run_shell")); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); t = 2000; expect(store.interruptOne(session.id).ok).toBe(true); @@ -851,7 +853,7 @@ describe("interrupt stamps finishedAt once", () => { test("sendInputOne interrupt starts a live follow-up turn and keeps tools", async () => { let t = 1000; - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; const store = createSubAgentSessionStore({ now: () => t, createId: () => "s-send", @@ -864,7 +866,7 @@ describe("interrupt stamps finishedAt once", () => { }); store.markRunning(session.id); store.appendEvent(session.id, startCall(1, "call-1", "run_shell")); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); store.registerFollowup( session.id, () => @@ -896,7 +898,7 @@ describe("interrupt stamps finishedAt once", () => { test("a follow-up turn keeps the lane live past the linger window until it completes", async () => { let t = 1000; - let finish: (reply: string) => void = () => {}; + let finish: (reply: string) => void = () => undefined; const store = createSubAgentSessionStore({ now: () => t, createId: () => "s-followup", @@ -908,7 +910,7 @@ describe("interrupt stamps finishedAt once", () => { retained: true, }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); store.registerFollowup( session.id, () => @@ -930,7 +932,7 @@ describe("interrupt stamps finishedAt once", () => { const live = store.list(); expect(live[0]?.lifecycleStatus).toBe("running"); expect(live[0]?.finishedAt).toBeUndefined(); - expect(agentLaneIsLive(live[0]!)).toBe(true); + expect(agentLaneIsLive(defined(live[0]))).toBe(true); expect(formatAgentsPanel(live, undefined, t)?.[0]?.status).toBe("running"); expect(fleetProgress(live, t).running).toBe(1); @@ -941,7 +943,7 @@ describe("interrupt stamps finishedAt once", () => { expect(terminal[0]?.status).toBe("done"); expect(terminal[0]?.lifecycleStatus).toBe("completed"); expect(terminal[0]?.finishedAt).toBe(12_000); - expect(agentLaneIsLive(terminal[0]!)).toBe(false); + expect(agentLaneIsLive(defined(terminal[0]))).toBe(false); expect(fleetProgress(terminal, t).running).toBe(0); }); }); @@ -951,7 +953,7 @@ describe("CL-7269 one stored worker lifecycle", () => { const store = createSubAgentSessionStore(); const session = store.start({ description: "d", agentId: "a", brief: "b", retained: true }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); expect(store.interruptOne(session.id).ok).toBe(true); store.complete(session.id, "late original send"); const after = store.get(session.id); @@ -1031,7 +1033,7 @@ describe("CL-7269 one stored worker lifecycle", () => { retained: true, }); store.markRunning(interrupted.id); - store.registerInterrupt(interrupted.id, () => {}); + store.registerInterrupt(interrupted.id, () => undefined); store.registerFollowup(interrupted.id, async () => "next"); expect(store.interruptOne(interrupted.id).ok).toBe(true); const afterInterrupt = store.get(interrupted.id); @@ -1192,7 +1194,7 @@ describe("pending ask_director", () => { retained: true, }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); store.registerFollowup(session.id, async () => "next"); let rejected: unknown; store.registerAsk(session.id, { @@ -1225,7 +1227,7 @@ describe("pending ask_director", () => { store.registerDeliver(session.id, (message) => { delivered.push(message); }); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); const followups: string[] = []; store.registerFollowup(session.id, async (message) => { followups.push(message); @@ -1271,7 +1273,7 @@ describe("pending ask_director", () => { }); store.markRunning(parent.id); store.markRunning(child.id); - store.registerInterrupt(parent.id, () => {}); + store.registerInterrupt(parent.id, () => undefined); store.registerFollowup(parent.id, async () => "next"); let rejected: unknown; store.registerAsk(child.id, { @@ -1337,7 +1339,7 @@ describe("pending ask_director", () => { retained: true, }); store.markRunning(session.id); - store.registerInterrupt(session.id, () => {}); + store.registerInterrupt(session.id, () => undefined); expect(store.interruptOne(session.id).ok).toBe(true); expect( @@ -1394,7 +1396,7 @@ describe("pending ask_director", () => { retained: true, }); store.markRunning(session.id); - store.registerClose(session.id, async () => {}); + store.registerClose(session.id, async () => undefined); let rejected: unknown; expect( store.registerAsk(session.id, { @@ -1420,7 +1422,7 @@ describe("pending ask_director", () => { brief: "b", retained: true, }); - store.registerClose(fill.id, async () => {}); + store.registerClose(fill.id, async () => undefined); store.complete(fill.id, "done"); } @@ -1484,7 +1486,7 @@ describe("pending ask_director", () => { const closed = store.start({ description: "x", agentId: "a", brief: "b", retained: true }); store.markRunning(closed.id); - store.registerClose(closed.id, async () => {}); + store.registerClose(closed.id, async () => undefined); let closeRejected: unknown; store.registerAsk(closed.id, { question: "q", diff --git a/src/subagent/session-store.ts b/src/subagent/session-store.ts index 7d1dda952..7dbe0c2b6 100644 --- a/src/subagent/session-store.ts +++ b/src/subagent/session-store.ts @@ -1372,7 +1372,7 @@ export function createSubAgentSessionStore( if (session.lifecycle.state !== "running") return false; if (pendingAsks.has(id)) return false; pendingAsks.set(id, ask); - mutate(id, () => {}); + mutate(id, () => undefined); return true; }, diff --git a/src/subagent/shell-evidence.ts b/src/subagent/shell-evidence.ts index 6a2548a3b..b402cae86 100644 --- a/src/subagent/shell-evidence.ts +++ b/src/subagent/shell-evidence.ts @@ -75,7 +75,8 @@ const EVIDENCE_VALUE_FLAGS: ReadonlySet = new Set([ function firstOperand(args: readonly string[], skip: number): string | undefined { let skipped = 0; for (let i = 0; i < args.length; i++) { - const arg = args[i]!; + const arg = args[i]; + if (arg === undefined) continue; if (arg === "--") continue; if (arg.startsWith("-")) { if (EVIDENCE_VALUE_FLAGS.has(arg)) i += 1; @@ -94,7 +95,9 @@ function classifySegment(segment: string, evidence: ShellFileEvidence): void { const tokens = tokenizeSegment(segment); if (tokens.length === 0) return; - const program = programBasename(tokens[0]!); + const head = tokens[0]; + if (head === undefined) return; + const program = programBasename(head); const args = tokens.slice(1); if (SHELL_READ_PROGRAMS.has(program)) { diff --git a/src/subagent/spawn-agent-worktree.test.ts b/src/subagent/spawn-agent-worktree.test.ts index 74437a4e7..a05932a17 100644 --- a/src/subagent/spawn-agent-worktree.test.ts +++ b/src/subagent/spawn-agent-worktree.test.ts @@ -12,6 +12,7 @@ import { createPermissionGate } from "../permission/gate.js"; import type { RunSubAgentParams, RunSubAgentResult } from "./types.js"; import type { Telemetry } from "../telemetry/index.js"; import { initTemporaryGitRepo } from "../../tests/helpers/temporary-git-repo.js"; +import { defined } from "../../tests/helpers/defined.js"; const run = promisify(execFile); @@ -35,8 +36,8 @@ function telemetryCapture() { installationId: "test", capture: (event, properties = {}) => events.push({ event, properties }), captureIntentional: () => false, - flush: async () => {}, - discard: () => {}, + flush: async () => undefined, + discard: () => undefined, }; return { telemetry, events }; } @@ -80,7 +81,7 @@ function deferred(): { promise: Promise; resolve: (v: T) => void; } { - let resolve!: (v: T) => void; + let resolve: (v: T) => void = () => undefined; const promise = new Promise((res) => { resolve = res; }); @@ -258,10 +259,10 @@ describe("spawn_agent worktree isolation", () => { run: async (params) => { workerCwd = params.cwd; params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); return settle.promise; }, @@ -285,11 +286,11 @@ describe("spawn_agent worktree isolation", () => { await waitFor(() => workerCwd !== undefined); expect(workerCwd).toBeDefined(); - expect(await pathExists(workerCwd!)).toBe(true); + expect(await pathExists(defined(workerCwd))).toBe(true); await sessions.closeOne(agentId, 1000); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(await pathExists(workerCwd!)).toBe(false); + expect(await pathExists(defined(workerCwd))).toBe(false); }); test("defers worktree cleanup while the session is interrupted for followup", async () => { @@ -314,10 +315,10 @@ describe("spawn_agent worktree isolation", () => { run: async (params) => { workerCwd = params.cwd; params.onAgentReady?.({ - close: async () => {}, - interrupt: () => {}, + close: async () => undefined, + interrupt: () => undefined, followup: async () => "", - deliver: () => {}, + deliver: () => undefined, }); const result = await settle.promise; const summary = Object.freeze({ @@ -374,11 +375,11 @@ describe("spawn_agent worktree isolation", () => { stop_reason: "interrupted", }); expect(workerCwd).toBeDefined(); - expect(await pathExists(workerCwd!)).toBe(true); + expect(await pathExists(defined(workerCwd))).toBe(true); await sessions.closeOne(agentId, 1000); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(await pathExists(workerCwd!)).toBe(false); + expect(await pathExists(defined(workerCwd))).toBe(false); }); test("reclaims the worktree immediately when the agent is not retained", async () => { diff --git a/src/subagent/tool-preview.test.ts b/src/subagent/tool-preview.test.ts index 8d4c60602..8c397dc53 100644 --- a/src/subagent/tool-preview.test.ts +++ b/src/subagent/tool-preview.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { TOOL_PREVIEW_MAX, toolCallPreview } from "./tool-preview"; +import { defined } from "../../tests/helpers/defined.js"; describe("toolCallPreview", () => { test("a shell call's subject is the command, not the tool name", () => { @@ -52,8 +53,8 @@ describe("toolCallPreview", () => { const command = "z".repeat(TOOL_PREVIEW_MAX + 20); const preview = toolCallPreview("run_shell", JSON.stringify({ command })); expect(preview).not.toBeNull(); - expect(preview!.length).toBe(TOOL_PREVIEW_MAX); - expect(preview!.endsWith("…")).toBe(true); + expect(defined(preview).length).toBe(TOOL_PREVIEW_MAX); + expect(defined(preview).endsWith("…")).toBe(true); }); test("newlines collapse to a single-line subject", () => { diff --git a/src/subagent/trace-reader.test.ts b/src/subagent/trace-reader.test.ts index 3519dcc92..79d65232a 100644 --- a/src/subagent/trace-reader.test.ts +++ b/src/subagent/trace-reader.test.ts @@ -12,6 +12,7 @@ import { MAX_TRACE_TOTAL_CHARS, MAX_TRACE_TURN_WINDOW, } from "./trace-reader.js"; +import { defined } from "../../tests/helpers/defined.js"; function tempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), "trace-reader-")); @@ -32,7 +33,7 @@ describe("listUniqueSubdirs", () => { const entries = await listUniqueSubdirs(root); expect(entries).toHaveLength(1); - expect(entries[0]!.path).toBe(fs.realpathSync(real)); + expect(defined(entries[0]).path).toBe(fs.realpathSync(real)); }); test("two distinct real directories are both listed", async () => { @@ -121,7 +122,7 @@ describe("readAgentTrace", () => { const result = await readAgentTrace(root, "worker-1"); expect(result.totalTurns).toBe(3); expect(result.entries.map((e) => e.kind)).toEqual(["text", "tool_call", "error"]); - expect(result.entries[2]!.isError).toBe(true); + expect(defined(result.entries[2]).isError).toBe(true); expect(result.omitted).toBeNull(); }); @@ -151,7 +152,7 @@ describe("readAgentTrace", () => { expect(result.entries).toHaveLength(2); expect(result.entriesTruncated).toBe(true); expect(result.omitted).not.toBeNull(); - expect(result.omitted!.hint.length).toBeGreaterThan(0); + expect(defined(result.omitted).hint.length).toBeGreaterThan(0); }); test("never exceeds the total-output character cap regardless of entry/window caps", async () => { @@ -172,7 +173,7 @@ describe("readAgentTrace", () => { expect(totalChars).toBeLessThanOrEqual(MAX_TRACE_TOTAL_CHARS); expect(result.entriesTruncated).toBe(true); expect(result.omitted).not.toBeNull(); - expect(result.omitted!.reason).toContain("total output cap"); + expect(defined(result.omitted).reason).toContain("total output cap"); }); test("never exceeds the hard entry-limit cap regardless of requested limit", async () => { @@ -230,8 +231,8 @@ describe("readAgentTrace", () => { ]); const result = await readAgentTrace(root, "worker-1"); - expect(result.entries[0]!.truncated).toBe(true); - expect(result.entries[0]!.content.length).toBeLessThan(10_000); + expect(defined(result.entries[0]).truncated).toBe(true); + expect(defined(result.entries[0]).content.length).toBeLessThan(10_000); }); test("a partially written trace (worker still running) reads what exists so far", async () => { diff --git a/src/subagent/trace-reader.ts b/src/subagent/trace-reader.ts index d5e0959f7..1fee83cfd 100644 --- a/src/subagent/trace-reader.ts +++ b/src/subagent/trace-reader.ts @@ -347,7 +347,8 @@ export async function readAgentTrace( let lastReadTurn = fromTurn; outer: for (let i = fromTurn; i < toTurn; i++) { lastReadTurn = i; - const turn = turns[i]!; + const turn = turns[i]; + if (turn === undefined) continue; for (const block of turn.content) { const entry = blockToEntry(i, turn.role, block); if (entry === null) continue; diff --git a/src/subagent/worktree.test.ts b/src/subagent/worktree.test.ts index fc61d7ebb..a9dbbac13 100644 --- a/src/subagent/worktree.test.ts +++ b/src/subagent/worktree.test.ts @@ -6,6 +6,7 @@ import { WorktreeError, type WorktreeExec, } from "./worktree.js"; +import { defined } from "../../tests/helpers/defined.js"; function recordingExec(responses: Record): { exec: WorktreeExec; @@ -17,7 +18,7 @@ function recordingExec(responses: Record Date: Wed, 9 Sep 2026 22:39:53 -0700 Subject: [PATCH 05/10] Replace remaining empty functions and non-null assertions --- evals/capability/behaviors.ts | 9 +- evals/capability/lib.test.ts | 65 ++--- evals/capability/lib.ts | 38 ++- scripts/approval-forensics.ts | 14 +- scripts/eval-capability.ts | 12 +- scripts/eval-public-swe-one.ts | 3 +- scripts/guard-real-projects-dir.ts | 2 +- scripts/intervention-forensics.ts | 9 +- src/agent/agent-search.test.ts | 3 +- src/agent/background-shell-tool.test.ts | 5 +- src/agent/codex-apply-patch.test.ts | 17 +- src/agent/codex-apply-patch.ts | 48 ++-- src/agent/codex-tool-mount.test.ts | 2 +- src/agent/codex-tool-proxies.test.ts | 11 +- src/agent/codex-tool-proxies.ts | 11 +- src/agent/compaction.test.ts | 42 +-- src/agent/director.test.ts | 18 +- src/agent/retry-policy.test.ts | 14 +- src/agent/tool-schema-normalize.test.ts | 19 +- src/agent/tools.ts | 6 +- src/auth/codex/usage-limit-error.test.ts | 3 +- src/auth/oauth/oauth.test.ts | 2 +- src/changelog/index.test.ts | 15 +- src/changelog/index.ts | 30 ++- src/config.test.ts | 30 ++- src/config/index.ts | 5 +- src/context-compactor.test.ts | 65 ++--- src/crash/report.test.ts | 5 +- src/director.test.ts | 72 +++--- src/exec/runner.ts | 8 +- src/extensions/skills.ts | 8 +- src/mcp/client-auth-policy.test.ts | 3 +- src/mcp/client-auth-reauth-cap.test.ts | 4 +- src/mcp/plugin.test.ts | 7 +- src/mcp/tool-name.ts | 9 +- src/mcp/tool-permissions.ts | 4 +- src/perf/assert-spans.test.ts | 20 +- src/perf/attribution-report.test.ts | 25 +- src/perf/index.test.ts | 63 ++--- src/perf/otel-sink.test.ts | 21 +- src/perf/permission-subagent-spans.test.ts | 21 +- src/perf/reactor-spans.test.ts | 59 ++--- src/perf/rollup.test.ts | 27 +- src/perf/rollup.ts | 7 +- src/permission/approval-log.test.ts | 37 +-- src/permission/approval-log.ts | 4 +- src/permission/auto-shell-policy.ts | 13 +- src/permission/classify.ts | 3 +- src/permission/command.ts | 3 +- src/permission/gate.ts | 6 +- src/permission/permission.test.ts | 7 +- src/plugins/agent-plugins.test.ts | 9 +- src/plugins/agent-plugins.ts | 2 +- src/plugins/change-diff.test.ts | 7 +- src/plugins/change-diff.ts | 55 +++- src/plugins/claude-plugins.test.ts | 19 +- src/plugins/data-only-agent.ts | 3 +- src/plugins/data-only-commands.ts | 2 +- src/plugins/delete-file-plugin.test.ts | 2 +- src/plugins/diagnostics.test.ts | 5 +- .../edit-file-diagnostics-plugin.test.ts | 19 +- src/plugins/edit-file-diagnostics-plugin.ts | 3 +- src/plugins/loader.test.ts | 17 +- src/plugins/loader.ts | 8 +- src/plugins/read-file-guard-plugin.test.ts | 11 +- src/plugins/register.ts | 7 +- src/plugins/result-truncation-plugin.test.ts | 13 +- src/plugins/shell-guard-plugin.test.ts | 69 ++--- src/plugins/shell-guard-plugin.ts | 3 +- src/plugins/skill-commands.ts | 2 +- src/plugins/tool-plugins.test.ts | 3 +- src/plugins/tool-result-secret-scrub.test.ts | 5 +- src/pricing-fetcher.test.ts | 7 +- src/prompts.test.ts | 2 +- .../openai-compatible-adapter.test.ts | 3 +- src/provider/reasoning-effort.ts | 4 +- src/provider/replay-sanitizer.test.ts | 3 +- src/session/active-host.test.ts | 8 +- src/session/assemble-runtime.test.ts | 8 +- src/session/attachment-store.test.ts | 11 +- src/session/attachment-store.ts | 10 +- src/session/attachment-uri.ts | 5 +- src/session/compactor.ts | 40 ++- src/session/hooks.test.ts | 4 +- src/session/hooks.ts | 6 +- src/session/incremental-jsonl.ts | 34 ++- src/session/index.ts | 9 +- src/session/optimized-context-store.test.ts | 11 +- src/session/optimized-context-store.ts | 12 +- src/session/run-sink.test.ts | 6 +- src/session/runtime-assembly.ts | 6 +- src/session/state.ts | 2 +- src/session/stream-consumer.test.ts | 2 +- src/session/stream-journal.test.ts | 2 +- src/settings.test.ts | 2 +- src/shell/background-shell.test.ts | 5 +- src/shell/persistent-shell-cwd.ts | 3 +- src/shell/run-shell-authz.ts | 79 ++++-- src/telemetry/ai-observability.test.ts | 4 +- src/telemetry/index.ts | 6 +- src/upgrade/index.test.ts | 3 +- src/web/plugin-provider.test.ts | 3 +- src/web/secret-scrub.ts | 5 +- src/workflows/coordinator.ts | 2 +- .../crash-run/simulate-run-end-crash.ts | 2 +- tests/fixtures/crash-run/simulate-signal.ts | 2 +- tests/fixtures/plugins/exa/src/index.test.ts | 12 +- tests/fixtures/tier-xhard/src/notify.ts | 2 +- .../fixtures/tier-xhard/tests/notify.test.ts | 2 +- tests/integration/harness.ts | 4 +- .../reactor-approval-suspend.test.ts | 15 +- tests/unit/agent-context-extensions.test.ts | 3 +- tests/unit/agent-tools.test.ts | 2 +- tests/unit/codex-sse-fixtures.test.ts | 10 +- tests/unit/compactor-pairing.test.ts | 3 +- tests/unit/corbits-skills-catalog.test.ts | 3 +- tests/unit/data-only-agent.test.ts | 242 ++++++++++-------- tests/unit/data-only-commands.test.ts | 77 +++--- tests/unit/director.test.ts | 6 +- tests/unit/example-agent-plugin.test.ts | 10 +- tests/unit/exec/runner.test.ts | 7 +- tests/unit/index.test.ts | 5 +- tests/unit/mcp.test.ts | 29 ++- tests/unit/path-plugin-trust.test.ts | 8 +- tests/unit/plugin-loader-path.test.ts | 10 +- tests/unit/plugin-marketplace.test.ts | 3 +- tests/unit/project-trust-plugins.test.ts | 2 +- tests/unit/ripgrep-plugin.test.ts | 5 +- tests/unit/skill-commands.test.ts | 56 ++-- tests/unit/skills.test.ts | 9 +- tests/unit/subagent-session-store.test.ts | 2 +- tests/unit/telemetry-product-events.test.ts | 3 +- tests/unit/telemetry-singleton.test.ts | 4 +- tests/unit/telemetry-toggle.test.ts | 19 +- tests/unit/telemetry.test.ts | 31 +-- tests/unit/tui/agent-tools.test.ts | 38 +-- tests/unit/tui/at-mention-resolution.test.ts | 2 +- tests/unit/tui/run-sink.test.ts | 7 +- tests/unit/tui/runner.test.ts | 9 +- tests/unit/vendor-patch-ledger.test.ts | 5 +- tests/unit/workflow-host.test.ts | 15 +- tests/unit/workflows-definitions.test.ts | 3 +- tests/unit/workflows-director.test.ts | 16 +- .../workflows-runtime-persistence.test.ts | 5 +- 144 files changed, 1276 insertions(+), 917 deletions(-) diff --git a/evals/capability/behaviors.ts b/evals/capability/behaviors.ts index add00a0e7..b9cdcb396 100644 --- a/evals/capability/behaviors.ts +++ b/evals/capability/behaviors.ts @@ -137,8 +137,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 +168,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 ( diff --git a/evals/capability/lib.test.ts b/evals/capability/lib.test.ts index 8663df198..cb6004f08 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"; @@ -304,15 +305,15 @@ describe("parseMatrix", () => { 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", () => { @@ -344,20 +345,20 @@ describe("parseMatrix", () => { // "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"); }); }); @@ -421,7 +422,7 @@ 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); @@ -430,7 +431,7 @@ describe("computeCellAggregates", () => { 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", () => { @@ -438,7 +439,7 @@ describe("computeCellAggregates", () => { 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); }); }); @@ -498,7 +499,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 +536,7 @@ 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 +566,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 +624,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,7 +665,7 @@ 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, }); @@ -673,15 +674,15 @@ describe("resolveRequestedProviderModel", () => { const fallback = detectProviderFallback({ ...(requested.provider !== undefined ? { requestedProvider: requested.provider } : {}), ...(requested.model !== undefined ? { requestedModel: requested.model } : {}), - resolvedProvider: cell!.provider, - resolvedModel: cell!.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"); }); @@ -736,7 +737,7 @@ describe("compareToBaseline provider/model guard", () => { describe("baitReproduces", () => { test("null when the metric was never captured", () => { - const cell = computeCellAggregates([sampleResult({ behaviors: null })])[0]!; + const cell = defined(computeCellAggregates([sampleResult({ behaviors: null })])[0]); expect(baitReproduces(cell, { metric: "repeatedSearchCount", threshold: 0 })).toBeNull(); }); }); @@ -754,10 +755,10 @@ describe("parseEvalRunReport", () => { ], }); 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 +777,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 +799,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 +812,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 +823,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 +856,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..056049764 100644 --- a/evals/capability/lib.ts +++ b/evals/capability/lib.ts @@ -585,16 +585,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( @@ -751,7 +765,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 { @@ -776,7 +799,8 @@ 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> = {}; for (const metric of NUMERIC_BEHAVIOR_METRICS) { diff --git a/scripts/approval-forensics.ts b/scripts/approval-forensics.ts index bd9575c75..892d0c349 100644 --- a/scripts/approval-forensics.ts +++ b/scripts/approval-forensics.ts @@ -45,7 +45,9 @@ 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 value = sorted[index]; + if (value === undefined) return 0; + return value; } interface Bucket { @@ -133,14 +135,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( diff --git a/scripts/eval-capability.ts b/scripts/eval-capability.ts index 9a7c6b8d4..fa2a74f84 100755 --- a/scripts/eval-capability.ts +++ b/scripts/eval-capability.ts @@ -159,7 +159,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); @@ -180,7 +182,8 @@ export function parseArgs(argv: readonly string[]): CliOptions { 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`); @@ -942,7 +945,10 @@ async function main(): Promise { 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); diff --git a/scripts/eval-public-swe-one.ts b/scripts/eval-public-swe-one.ts index 1e92eff9f..8953b0d2f 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}`); diff --git a/scripts/guard-real-projects-dir.ts b/scripts/guard-real-projects-dir.ts index 177bfcdce..f3da6e37e 100644 --- a/scripts/guard-real-projects-dir.ts +++ b/scripts/guard-real-projects-dir.ts @@ -64,7 +64,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..2445d45cd 100644 --- a/scripts/intervention-forensics.ts +++ b/scripts/intervention-forensics.ts @@ -61,7 +61,9 @@ 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 value = sorted[index]; + if (value === undefined) return 0; + return value; } interface Bucket { @@ -172,10 +174,11 @@ const rows = [...buckets.entries()].sort((a, b) => b[1].count - a[1].count); 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]!}`; + : `${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)}`, diff --git a/src/agent/agent-search.test.ts b/src/agent/agent-search.test.ts index b24331746..87ef2dce7 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 { @@ -42,7 +43,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="); }); diff --git a/src/agent/background-shell-tool.test.ts b/src/agent/background-shell-tool.test.ts index 33125b151..30a298cd2 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"; @@ -63,8 +64,8 @@ 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(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); diff --git a/src/agent/codex-apply-patch.test.ts b/src/agent/codex-apply-patch.test.ts index 8ff0c5da6..d7c1abb11 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, @@ -49,14 +50,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 +72,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" }, ]); @@ -186,7 +187,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 +211,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..1b704d46b 100644 --- a/src/agent/codex-apply-patch.ts +++ b/src/agent/codex-apply-patch.ts @@ -100,18 +100,24 @@ 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". @@ -131,19 +137,23 @@ export function parseCodexApplyPatch(input: string): ParsedPatch { 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( @@ -247,7 +257,10 @@ export function contentFromAddOp(op: PatchAddOp): string { } function parseHunk(body: string[], start: number): { hunk: PatchHunk; next: number } { - const headerLine = body[start]!; + const headerLine = body[start]; + if (headerLine === undefined) { + throw new CodexApplyPatchError("expected hunk start '@@'"); + } let header: string | undefined; if (headerLine === "@@") { header = undefined; @@ -262,7 +275,8 @@ function parseHunk(body: string[], start: number): { hunk: PatchHunk; next: numb 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 { @@ -337,10 +351,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; } @@ -365,7 +381,9 @@ function findSequence( 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; } diff --git a/src/agent/codex-tool-mount.test.ts b/src/agent/codex-tool-mount.test.ts index 2c5e978c9..2e4e0f08a 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"); diff --git a/src/agent/codex-tool-proxies.test.ts b/src/agent/codex-tool-proxies.test.ts index 525b9393e..7e818b1de 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"; @@ -128,7 +129,7 @@ describe("createCodexToolProxies", () => { }); 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"], }); }); @@ -288,7 +289,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") @@ -319,11 +320,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!") diff --git a/src/agent/codex-tool-proxies.ts b/src/agent/codex-tool-proxies.ts index 8bb2628a3..7c0905b0c 100644 --- a/src/agent/codex-tool-proxies.ts +++ b/src/agent/codex-tool-proxies.ts @@ -349,12 +349,17 @@ 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(" "); } diff --git a/src/agent/compaction.test.ts b/src/agent/compaction.test.ts index 8a35a790e..541a85675 100644 --- a/src/agent/compaction.test.ts +++ b/src/agent/compaction.test.ts @@ -133,7 +133,7 @@ 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(); @@ -142,7 +142,7 @@ describe("compaction governor", () => { }); 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); @@ -157,7 +157,7 @@ describe("compaction governor", () => { }); test("recovers from context overflow a bounded number of times", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); expect(governor.interceptOverflow(overflowError(), capabilities)).not.toBeNull(); expect(governor.resumeAfterCompact(emptyMessage())).toBe("infer"); expect(governor.interceptOverflow(overflowError(), capabilities)).not.toBeNull(); @@ -254,7 +254,7 @@ describe("compaction governor", () => { }); 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); @@ -265,7 +265,7 @@ describe("compaction governor", () => { }); 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); @@ -274,7 +274,7 @@ describe("compaction governor", () => { }); test("stays inert when usage is missing but the accumulated estimate is small", () => { - const governor = createCompactionGovernor(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDoneWithoutUsage(), turnsOfLength(10, 4)); expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); }); @@ -283,7 +283,7 @@ describe("compaction governor", () => { // 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); @@ -294,7 +294,7 @@ describe("compaction governor", () => { }); 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); @@ -306,7 +306,7 @@ describe("compaction governor", () => { }); 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,7 +319,7 @@ 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), @@ -333,7 +333,7 @@ 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(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold * 10), turnsOfLength(2, 1)); expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); }); @@ -343,7 +343,7 @@ 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(); @@ -360,14 +360,14 @@ 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(() => {}); + 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(() => {}); + const governor = createCompactionGovernor(() => undefined); governor.noteInferenceDone(inferenceDone(overThreshold), turnsOfLength(floor + 1, 1)); const actions = governor.interceptActions(toolDone(), inferAction, capabilities); expect(actions).not.toBeNull(); @@ -382,7 +382,7 @@ 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(); @@ -395,7 +395,7 @@ describe("compaction governor", () => { }); 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,7 +414,7 @@ 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(); @@ -425,7 +425,7 @@ describe("compaction governor", () => { }); 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(); @@ -439,7 +439,7 @@ describe("compaction governor", () => { }); 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(); @@ -457,7 +457,7 @@ describe("compaction governor", () => { }); 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(); @@ -470,7 +470,7 @@ 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(); diff --git a/src/agent/director.test.ts b/src/agent/director.test.ts index 6cc6c3db5..03c76084c 100644 --- a/src/agent/director.test.ts +++ b/src/agent/director.test.ts @@ -82,7 +82,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,7 +96,7 @@ 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(); @@ -113,7 +113,7 @@ 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(); @@ -153,7 +153,7 @@ 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(); @@ -171,7 +171,7 @@ 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(); @@ -193,7 +193,7 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { 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,7 +210,7 @@ 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(); @@ -246,7 +246,7 @@ 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(); @@ -263,7 +263,7 @@ 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(); diff --git a/src/agent/retry-policy.test.ts b/src/agent/retry-policy.test.ts index 0feaed8d4..076370a41 100644 --- a/src/agent/retry-policy.test.ts +++ b/src/agent/retry-policy.test.ts @@ -6,10 +6,10 @@ 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, }; @@ -252,12 +252,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({ diff --git a/src/agent/tool-schema-normalize.test.ts b/src/agent/tool-schema-normalize.test.ts index becf2e919..8cf8cbc3e 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"; @@ -52,9 +53,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?: { @@ -71,8 +72,8 @@ describe("normalizeToolDefinitionsForProvider", () => { 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,7 +84,7 @@ 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, ); @@ -96,7 +97,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 +107,7 @@ 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 +119,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/tools.ts b/src/agent/tools.ts index 3e1ccb5bb..60af74922 100644 --- a/src/agent/tools.ts +++ b/src/agent/tools.ts @@ -542,7 +542,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({ diff --git a/src/auth/codex/usage-limit-error.test.ts b/src/auth/codex/usage-limit-error.test.ts index 32e382eb5..5db785c9b 100644 --- a/src/auth/codex/usage-limit-error.test.ts +++ b/src/auth/codex/usage-limit-error.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { codexUsageLimitRetryAfterMs, @@ -99,7 +100,7 @@ describe("formatCodexUsageLimitMessage", () => { test("names plan, reset ETA, and profile switch path", () => { const parsed = parseCodexUsageLimitError(LIVE_USAGE_LIMIT_BODY); expect(parsed).toBeDefined(); - const line = formatCodexUsageLimitMessage(parsed!, { profile: "abk-labs" }); + const line = formatCodexUsageLimitMessage(defined(parsed), { profile: "abk-labs" }); expect(line).toContain('Codex profile "abk-labs"'); expect(line).toContain("workspace member"); expect(line).toMatch(/Resets in ~/); diff --git a/src/auth/oauth/oauth.test.ts b/src/auth/oauth/oauth.test.ts index 8e79a38a7..e35c7b664 100644 --- a/src/auth/oauth/oauth.test.ts +++ b/src/auth/oauth/oauth.test.ts @@ -447,7 +447,7 @@ describe("createTokenSession", () => { const empty = createTokenSession({ skewMs: 100, loadProfile: async () => undefined, - updateTokens: async () => {}, + updateTokens: async () => undefined, refreshTokens: async () => ({ access: "x", refresh: "x", expiresAt: 0 }), toAccess: (tokens) => tokens.access, missingError: (name) => new Error(`missing ${name}`), diff --git a/src/changelog/index.test.ts b/src/changelog/index.test.ts index 9c8c6cbfa..b55cb7250 100644 --- a/src/changelog/index.test.ts +++ b/src/changelog/index.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -46,24 +47,24 @@ describe("parseChangelogText", () => { "0.2.85", "0.1.0", ]); - expect(entries[0]!.content).toContain("## [0.2.86]"); - expect(entries[0]!.content).toContain("Feature A"); + expect(defined(entries[0]).content).toContain("## [0.2.86]"); + expect(defined(entries[0]).content).toContain("Feature A"); expect(entries.every((e) => !e.content.includes("Unreleased"))).toBe(true); }); test("accepts unbracketed version headers", () => { const entries = parseChangelogText("## 1.2.3\n\n- note\n"); expect(entries).toHaveLength(1); - expect(entries[0]!.major).toBe(1); - expect(entries[0]!.minor).toBe(2); - expect(entries[0]!.patch).toBe(3); + expect(defined(entries[0]).major).toBe(1); + expect(defined(entries[0]).minor).toBe(2); + expect(defined(entries[0]).patch).toBe(3); }); }); describe("compareVersions / getNewEntries", () => { test("orders major.minor.patch", () => { - const a = parseVersionString("0.2.86")!; - const b = parseVersionString("0.2.85")!; + const a = defined(parseVersionString("0.2.86")); + const b = defined(parseVersionString("0.2.85")); expect(compareVersions(a, b)).toBeGreaterThan(0); expect(compareVersions(b, a)).toBeLessThan(0); expect(compareVersions(a, a)).toBe(0); diff --git a/src/changelog/index.ts b/src/changelog/index.ts index 07c47ed61..4f70c5bd1 100644 --- a/src/changelog/index.ts +++ b/src/changelog/index.ts @@ -39,12 +39,20 @@ export function parseChangelogText(content: string): ChangelogEntry[] { flush(); const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/); if (versionMatch !== null) { - currentVersion = { - major: Number.parseInt(versionMatch[1]!, 10), - minor: Number.parseInt(versionMatch[2]!, 10), - patch: Number.parseInt(versionMatch[3]!, 10), - }; - currentLines = [line]; + const major = versionMatch[1]; + const minor = versionMatch[2]; + const patch = versionMatch[3]; + if (major === undefined || minor === undefined || patch === undefined) { + currentVersion = null; + currentLines = []; + } else { + currentVersion = { + major: Number.parseInt(major, 10), + minor: Number.parseInt(minor, 10), + patch: Number.parseInt(patch, 10), + }; + currentLines = [line]; + } } else { currentVersion = null; currentLines = []; @@ -79,10 +87,14 @@ export function parseVersionString(version: string): ChangelogEntry | null { .replace(/^v/i, "") .match(/^(\d+)\.(\d+)\.(\d+)/); if (match === null) return null; + const major = match[1]; + const minor = match[2]; + const patch = match[3]; + if (major === undefined || minor === undefined || patch === undefined) return null; return { - major: Number.parseInt(match[1]!, 10), - minor: Number.parseInt(match[2]!, 10), - patch: Number.parseInt(match[3]!, 10), + major: Number.parseInt(major, 10), + minor: Number.parseInt(minor, 10), + patch: Number.parseInt(patch, 10), content: "", }; } diff --git a/src/config.test.ts b/src/config.test.ts index 45ca4c763..8237fb1c6 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../tests/helpers/defined.js"; import { afterEach, beforeEach, describe, test, expect } from "bun:test"; import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -339,8 +340,8 @@ describe("loadConfig", () => { expect(result.configured).toBe(false); if (result.configured === false) { expect(result.settingsDiagnostics).toBeDefined(); - expect(result.settingsDiagnostics!.length).toBeGreaterThan(0); - expect(result.settingsDiagnostics!.some((d) => /unknown/i.test(d.message))).toBe(true); + expect(defined(result.settingsDiagnostics).length).toBeGreaterThan(0); + expect(defined(result.settingsDiagnostics).some((d) => /unknown/i.test(d.message))).toBe(true); } } finally { await rm(cwd, { recursive: true, force: true }); @@ -616,16 +617,17 @@ describe("loadConfig", () => { ); } - let config: Awaited>; + let config: Awaited> | undefined; const logged = await withFileLogSink(async () => { config = await loadConfig(["resume", targetId, "--force", "--cwd", cwd], { globalSettingsPath: globalPath, home, }); }); - assertConfigured(config!); - expect(config!.sessionId).toBe(targetId); - expect(config!.task).toBe("target failed session"); + const loaded = defined(config, "config"); + assertConfigured(loaded); + expect(loaded.sessionId).toBe(targetId); + expect(loaded.task).toBe("target failed session"); expect(logged).not.toContain("unreadable session state"); expect(logged).not.toContain(home); } finally { @@ -1436,10 +1438,10 @@ describe("buildProviderCatalog", () => { }; const catalog = buildProviderCatalog(settings, resolved); expect(catalog.map((c) => c.name).sort()).toEqual(["fp", "oa"]); - const fp = catalog.find((c) => c.name === "fp")!; + const fp = defined(catalog.find((c) => c.name === "fp")); expect(fp.models).toEqual(["fp-large", "fp-small"]); expect(fp.defaultModel).toBe("fp-large"); - expect(catalog.find((c) => c.name === "oa")!.defaultModel).toBeUndefined(); + expect(defined(catalog.find((c) => c.name === "oa")).defaultModel).toBeUndefined(); }); test("normalizes provider base URLs from the settings file", () => { @@ -1468,7 +1470,7 @@ describe("buildProviderCatalog", () => { }, }; const catalog = buildProviderCatalog(settings, resolved); - const bf = catalog.find((c) => c.name === "bf")!; + const bf = defined(catalog.find((c) => c.name === "bf")); expect(bf.bifrostVirtualKey).toBe(true); }); @@ -1487,10 +1489,10 @@ describe("buildProviderCatalog", () => { }, }; const catalog = buildProviderCatalog(settings, resolved); - const ollama = catalog.find((c) => c.name === "ollama")!; + const ollama = defined(catalog.find((c) => c.name === "ollama")); expect(ollama.keyless).toBe(true); expect(ollama.apiKey).toBeUndefined(); - const fp = catalog.find((c) => c.name === "fp")!; + const fp = defined(catalog.find((c) => c.name === "fp")); expect(fp.keyless).toBeUndefined(); expect(fp.apiKey).toBe("fp-key"); }); @@ -1684,7 +1686,7 @@ describe("buildProviderCatalog", () => { apiKey: "fp-key", model: "fp-large", } as ResolvedProvider); - const entry = catalog.find((c) => c.name === "fp")!; + const entry = defined(catalog.find((c) => c.name === "fp")); const roundTripped = { fp: catalogEntryAsProviderSettings(entry) }; expect(roundTripped).toEqual({ fp: provider }); }); @@ -1703,7 +1705,7 @@ describe("buildProviderCatalog", () => { apiKey: "an-key", model: "claude", } as ResolvedProvider); - const entry = catalog.find((c) => c.name === "an")!; + const entry = defined(catalog.find((c) => c.name === "an")); const roundTripped = { an: catalogEntryAsProviderSettings(entry) }; expect(roundTripped).toEqual({ an: provider }); }); @@ -1722,7 +1724,7 @@ describe("buildProviderCatalog", () => { apiKey: "go-key", model: "go-model", } as ResolvedProvider); - const entry = catalog.find((c) => c.name === "go")!; + const entry = defined(catalog.find((c) => c.name === "go")); const roundTripped = { go: catalogEntryAsProviderSettings(entry) }; expect(roundTripped).toEqual({ go: provider }); }); diff --git a/src/config/index.ts b/src/config/index.ts index 3fd8d36c9..2a95f9f3b 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -900,7 +900,10 @@ export async function loadConfig( resumePicker = true; skipInitialTask = true; } else if (resumeMode === "id") { - const id = resumeSessionId!; + const id = resumeSessionId; + if (id === undefined) { + throw new Error("resume by id requires a session id"); + } await migrateLegacySessionIfNeeded(cwd, id, options.home); const loaded = await loadState(cwd, id, options.home); if (loaded.kind === "unreadable") { diff --git a/src/context-compactor.test.ts b/src/context-compactor.test.ts index e28c3e19b..523896637 100644 --- a/src/context-compactor.test.ts +++ b/src/context-compactor.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { createPruningCompactor, @@ -45,7 +46,7 @@ function allText(turns: ConversationTurn[]): string { } function hasConsecutiveSameRole(turns: ConversationTurn[]): boolean { - return turns.some((t, i) => i > 0 && turns[i - 1]!.role === t.role); + return turns.some((t, i) => i > 0 && defined(turns[i - 1]).role === t.role); } describe("createPruningCompactor", () => { @@ -98,7 +99,7 @@ describe("createPruningCompactor", () => { const result = await compactor.apply(turns, mockStrategyCtx); // Summary leads as a user turn (survives every adapter). - expect(result.output[0]!.role).toBe("user"); + expect(defined(result.output[0]).role).toBe("user"); expect(allText(result.output)).toContain("[Compacted prior context]"); // The initiating user message and the recent turn both survive. expect(allText(result.output)).toContain("old message 1"); @@ -129,11 +130,11 @@ describe("createPruningCompactor", () => { const result = await compactor.apply(turns, mockStrategyCtx); expect(result.output.length).toBe(2); - const recentTurn = result.output[1]!; + const recentTurn = defined(result.output[1]); expect(recentTurn.role).toBe("assistant"); const toolCalls = recentTurn.content.filter((b) => b.type === "tool_call"); expect(toolCalls.length).toBe(1); - expect(toolCalls[0]!.name).toBe("read_file"); + expect(defined(toolCalls[0]).name).toBe("read_file"); }); }); @@ -175,7 +176,7 @@ describe("createPruningCompactor — initiating task preservation", () => { makeTurn({ role: "user", content: [{ type: "text", text: "recent" }] }), ]; const result = await compactor.apply(turns, mockStrategyCtx); - expect(result.output[0]!.role).toBe("user"); + expect(defined(result.output[0]).role).toBe("user"); expect(result.output.every((t) => t.role !== "system")).toBe(true); }); @@ -274,10 +275,10 @@ describe("createPruningCompactor — image aging", () => { ), ).toBe(true); expect(result.blobs).toBeDefined(); - expect(result.blobs!.length).toBeGreaterThanOrEqual(1); - expect(result.blobs![0]!.contentType).toBe("image/png"); + expect(defined(result.blobs).length).toBeGreaterThanOrEqual(1); + expect(defined(defined(result.blobs)[0]).contentType).toBe("image/png"); // Blob payload is the original base64 (UTF-8), not lost. - expect(new TextDecoder().decode(result.blobs![0]!.bytes)).toBe("iVBORw0KGgo="); + expect(new TextDecoder().decode(defined(defined(result.blobs)[0]).bytes)).toBe("iVBORw0KGgo="); }); test("keeps an image intact when its turn is still within the recent window", async () => { @@ -316,7 +317,7 @@ describe("createPruningCompactor — image aging", () => { expect(JSON.stringify(result.output)).not.toContain("iVBORw0KGgo="); expect(result.blobs).toBeDefined(); - expect(result.blobs!.length).toBeGreaterThanOrEqual(1); + expect(defined(result.blobs).length).toBeGreaterThanOrEqual(1); expect( result.output.some((t) => t.content.some( @@ -615,15 +616,15 @@ describe("createPruningCompactor — prefix-stable summaries (CL-6914)", () => { const compactor = createPruningCompactor({ keepRecentTurns: 2, summaryMaxChars: 500 }); const turns = grow([], 16, "round1"); const output1 = (await compactor.apply(turns, mockStrategyCtx)).output; - expect(firstText(output1[0]!)).toContain(COMPACTED_PREFIX); + expect(firstText(defined(output1[0]))).toContain(COMPACTED_PREFIX); const output2 = (await compactor.apply(grow(output1, 16, "round2"), mockStrategyCtx)).output; - expect(firstText(output2[0]!)).toBe(firstText(output1[0]!)); + expect(firstText(defined(output2[0]))).toBe(firstText(defined(output1[0]))); expect(output2[0]).toBe(output1[0]); const summaries = compactedTurns(output2); expect(summaries.length).toBeGreaterThanOrEqual(2); - expect(output2.indexOf(summaries[1]!)).toBeGreaterThan(0); + expect(output2.indexOf(defined(summaries[1]))).toBeGreaterThan(0); expect(hasConsecutiveSameRole(output2)).toBe(false); expect( output2.some((t) => t.role === "assistant" && firstText(t) === COMPACT_SPACER_TEXT), @@ -636,9 +637,9 @@ describe("createPruningCompactor — prefix-stable summaries (CL-6914)", () => { const output2 = (await compactor.apply(grow(output1, 16, "round2"), mockStrategyCtx)).output; const spacer = output2.find(isHarnessCompactSpacer); expect(spacer).toBeDefined(); - expect(spacer!.model).toBe(HARNESS_COMPACT_SPACER_MODEL); - expect(firstText(spacer!)).toBe(COMPACT_SPACER_TEXT); - expect(firstText(spacer!)).not.toBe(LEGACY_COMPACT_SPACER_TEXT); + expect(defined(spacer).model).toBe(HARNESS_COMPACT_SPACER_MODEL); + expect(firstText(defined(spacer))).toBe(COMPACT_SPACER_TEXT); + expect(firstText(defined(spacer))).not.toBe(LEGACY_COMPACT_SPACER_TEXT); expect(COMPACT_SPACER_TEXT).not.toBe(LEGACY_COMPACT_SPACER_TEXT); }); @@ -652,23 +653,23 @@ describe("createPruningCompactor — prefix-stable summaries (CL-6914)", () => { model: "omen-alpha", content: [{ type: "text", text: LEGACY_COMPACT_SPACER_TEXT }], }); - const output2 = (await compactor.apply(grow([summary!, echo], 16, "round2"), mockStrategyCtx)) + const output2 = (await compactor.apply(grow([defined(summary), echo], 16, "round2"), mockStrategyCtx)) .output; let frozenLen = 0; while ( frozenLen < output2.length && - firstText(output2[frozenLen]!).startsWith(COMPACTED_PREFIX) + firstText(defined(output2[frozenLen])).startsWith(COMPACTED_PREFIX) ) { frozenLen++; - if (frozenLen < output2.length && isHarnessCompactSpacer(output2[frozenLen]!)) frozenLen++; + if (frozenLen < output2.length && isHarnessCompactSpacer(defined(output2[frozenLen]))) frozenLen++; } expect(output2.slice(0, frozenLen)).not.toContain(echo); expect(isHarnessCompactSpacer(echo)).toBe(false); const harness = output2.find(isHarnessCompactSpacer); expect(harness).toBeDefined(); - expect(harness!.model).toBe(HARNESS_COMPACT_SPACER_MODEL); - expect(firstText(harness!)).toBe(COMPACT_SPACER_TEXT); + expect(defined(harness).model).toBe(HARNESS_COMPACT_SPACER_MODEL); + expect(firstText(defined(harness))).toBe(COMPACT_SPACER_TEXT); expect(harness).not.toBe(echo); }); @@ -682,23 +683,23 @@ describe("createPruningCompactor — prefix-stable summaries (CL-6914)", () => { model: "omen-alpha", content: [{ type: "text", text: COMPACT_SPACER_TEXT }], }); - const output2 = (await compactor.apply(grow([summary!, echo], 16, "round2"), mockStrategyCtx)) + const output2 = (await compactor.apply(grow([defined(summary), echo], 16, "round2"), mockStrategyCtx)) .output; let frozenLen = 0; while ( frozenLen < output2.length && - firstText(output2[frozenLen]!).startsWith(COMPACTED_PREFIX) + firstText(defined(output2[frozenLen])).startsWith(COMPACTED_PREFIX) ) { frozenLen++; - if (frozenLen < output2.length && isHarnessCompactSpacer(output2[frozenLen]!)) frozenLen++; + if (frozenLen < output2.length && isHarnessCompactSpacer(defined(output2[frozenLen]))) frozenLen++; } expect(output2.slice(0, frozenLen)).not.toContain(echo); expect(isHarnessCompactSpacer(echo)).toBe(false); const harness = output2.find(isHarnessCompactSpacer); expect(harness).toBeDefined(); - expect(harness!.model).toBe(HARNESS_COMPACT_SPACER_MODEL); - expect(firstText(harness!)).toBe(COMPACT_SPACER_TEXT); + expect(defined(harness).model).toBe(HARNESS_COMPACT_SPACER_MODEL); + expect(firstText(defined(harness))).toBe(COMPACT_SPACER_TEXT); expect(harness).not.toBe(echo); }); @@ -711,7 +712,7 @@ describe("createPruningCompactor — prefix-stable summaries (CL-6914)", () => { role: "assistant", content: [{ type: "text", text: LEGACY_COMPACT_SPACER_TEXT }], }); - const grown = grow([summary!, legacySpacer], 16, "round2"); + const grown = grow([defined(summary), legacySpacer], 16, "round2"); const output2 = (await compactor.apply(grown, mockStrategyCtx)).output; expect(output2[0]).toBe(summary); expect(output2[1]).toBe(legacySpacer); @@ -763,12 +764,12 @@ describe("createPruningCompactor — prefix-stable summaries (CL-6914)", () => { }); const turns = grow([], 16, "fail"); const output1 = (await compactor.apply(turns, mockStrategyCtx)).output; - expect(firstText(output1[0]!)).toContain("Turns compacted:"); - expect(firstText(output1[0]!)).not.toContain("UNIQUE_SUCCESS_SUMMARY"); - expect(firstText(output1[0]!)).toContain("Model summary unavailable"); + expect(firstText(defined(output1[0]))).toContain("Turns compacted:"); + expect(firstText(defined(output1[0]))).not.toContain("UNIQUE_SUCCESS_SUMMARY"); + expect(firstText(defined(output1[0]))).toContain("Model summary unavailable"); const output2 = (await compactor.apply(grow(output1, 16, "ok"), mockStrategyCtx)).output; - expect(firstText(output2[0]!)).toBe(firstText(output1[0]!)); + expect(firstText(defined(output2[0]))).toBe(firstText(defined(output1[0]))); expect(allText(output2)).toContain("UNIQUE_SUCCESS_SUMMARY"); expect(hasConsecutiveSameRole(output2)).toBe(false); }); @@ -969,7 +970,7 @@ describe("buildTurnSummary via createPruningCompactor", () => { ]; const result = await compactor.apply(turns, mockStrategyCtx); - const summaryText = (result.output[0]!.content[0]! as { text: string }).text; + const summaryText = (defined(defined(result.output[0]).content[0]) as { text: string }).text; expect(summaryText).toContain("read_file"); expect(summaryText).toContain("Total tool calls: 1"); }); @@ -984,7 +985,7 @@ describe("buildTurnSummary via createPruningCompactor", () => { ]; const result = await compactor.apply(turns, mockStrategyCtx); - const summaryBlock = result.output[0]!.content[0]! as { text: string }; + const summaryBlock = defined(defined(result.output[0]).content[0]) as { text: string }; // The summary portion of the block is extracted from after the header line. // The header itself is "---..." so we look at the full block text — the // embedded buildTurnSummary output must end with "..." when truncated. diff --git a/src/crash/report.test.ts b/src/crash/report.test.ts index 23c029f54..e57b25329 100644 --- a/src/crash/report.test.ts +++ b/src/crash/report.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { afterEach, describe, expect, test } from "bun:test"; import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -67,7 +68,7 @@ describe("writeCrashReport", () => { const entries = await readdir(dir); expect(entries).toHaveLength(1); - const body = await readFile(join(dir, entries[0]!), "utf8"); + const body = await readFile(join(dir, defined(entries[0])), "utf8"); expect(body).toContain("kind: uncaughtException"); expect(body).toContain(`cwd: ${cwd}`); expect(body).toContain("boom"); @@ -76,7 +77,7 @@ describe("writeCrashReport", () => { test("returns null instead of throwing when the report cannot be written", async () => { // A path segment that is a file, not a directory, makes mkdir fail. home = await mkdtemp(join(tmpdir(), "corbits-crash-")); - primeCrashReporting("/Users/dev/some project!!", home, () => join(home!, "blocked-root")); + primeCrashReporting("/Users/dev/some project!!", home, () => join(defined(home), "blocked-root")); await Bun.write(join(home, "blocked-root"), "not a directory"); const file = await writeCrashReport("unhandledRejection", "oops", "/whatever", home); expect(file).toBeNull(); diff --git a/src/director.test.ts b/src/director.test.ts index 7914137b8..e19e7cebd 100644 --- a/src/director.test.ts +++ b/src/director.test.ts @@ -109,7 +109,7 @@ describe("operator declined tool calls", () => { // reactor and break further sends, and it does not re-infer off a bare // decline. test("chat director surfaces the decline and waits, keeping the reactor alive", async () => { - const director = createChatDirector("", [], { onTasksChange: () => {} }); + const director = createChatDirector("", [], { onTasksChange: () => undefined }); const actions = actionsArray( await director.decide(makeToolErrorEvent("c", declined), mockState, mockCapabilities), ); @@ -123,7 +123,7 @@ describe("operator declined tool calls", () => { // Reactor path: a reason-bearing approver rejection must re-infer so the // model can respond to the reason — never the canned decline. test("reason-bearing approver rejection re-infers on the reason", async () => { - const director = createChatDirector("", [], { onTasksChange: () => {} }); + const director = createChatDirector("", [], { onTasksChange: () => undefined }); const actions = actionsArray( await director.decide( makeToolErrorEvent("c", "denied by approver: never touch /etc"), @@ -137,7 +137,7 @@ describe("operator declined tool calls", () => { }); test("middleware rejection with a reason re-infers on the reason", async () => { - const director = createChatDirector("", [], { onTasksChange: () => {} }); + const director = createChatDirector("", [], { onTasksChange: () => undefined }); const actions = actionsArray( await director.decide( makeToolErrorEvent("c", `${declined} — only run it in the build sandbox`), @@ -153,7 +153,7 @@ describe("operator declined tool calls", () => { // Reactor path: a reason-less approver rejection has nothing for the model // to respond to; the canned reply stands. test("reason-less approver rejection takes the canned path", async () => { - const director = createChatDirector("", [], { onTasksChange: () => {} }); + const director = createChatDirector("", [], { onTasksChange: () => undefined }); const actions = actionsArray( await director.decide( makeToolErrorEvent("c", "denied by approver"), @@ -169,7 +169,7 @@ describe("operator declined tool calls", () => { // Policy denies and no-grant blocks are not operator decisions: the model // adapts to the deny text like any tool error. test("policy deny is not classified as an operator decline", async () => { - const director = createChatDirector("", [], { onTasksChange: () => {} }); + const director = createChatDirector("", [], { onTasksChange: () => undefined }); for (const content of [ "Denied by policy: tool:run_shell/invoke", "No matching grants for tool:run_shell/invoke", @@ -214,7 +214,7 @@ describe("open-task termination guard", () => { const hasReply = (a: ReactorAction[]): boolean => a.some((x) => x.type === "reply"); test("re-infers instead of ending the turn while a task is still open", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); const actions = actionsArray(await director.decide(textTurn(), mockState, mockCapabilities)); @@ -223,7 +223,7 @@ describe("open-task termination guard", () => { }); test("ends the turn normally once every task is terminal", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("done"), mockState, mockCapabilities); const actions = actionsArray(await director.decide(textTurn(), mockState, mockCapabilities)); @@ -232,7 +232,7 @@ describe("open-task termination guard", () => { }); test("stops nudging and lets the turn end after the cap of content-free attempts", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); for (let i = 0; i < 3; i++) { @@ -247,7 +247,7 @@ describe("open-task termination guard", () => { test("live fleet with open tasks allows terminal wait/reply and does not spend the nudge budget", async () => { let live = 1; const director = createChatDirector("base", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, getLiveFleetCount: () => live, }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); @@ -270,14 +270,14 @@ describe("open-task termination guard", () => { }); test("omitted or zero live fleet count still nudges while a task is open", async () => { - const omitted = createChatDirector("base", [], { onTasksChange: () => {} }); + const omitted = createChatDirector("base", [], { onTasksChange: () => undefined }); await omitted.decide(manageTasksEvent("doing"), mockState, mockCapabilities); expect( hasInfer(actionsArray(await omitted.decide(textTurn(), mockState, mockCapabilities))), ).toBe(true); const zero = createChatDirector("base", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, getLiveFleetCount: () => 0, }); await zero.decide(manageTasksEvent("doing"), mockState, mockCapabilities); @@ -289,7 +289,7 @@ describe("open-task termination guard", () => { test("empty model turn settles with a valid empty reply", async () => { // DefaultDirector ends empty responses with bare wait; without a reply, // agent.send hangs and the TUI Working spinner sticks forever. - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); const emptyTurn = { type: "inference.done", turn: { role: "assistant", model: "test", timestamp: 0, content: [] }, @@ -307,7 +307,7 @@ describe("open-task termination guard", () => { }); test("a declined tool with open tasks re-infers, then terminates after its cap", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); for (let i = 0; i < 2; i++) { @@ -335,7 +335,7 @@ describe("open-task termination guard", () => { // single user turn — the budget is monotonic per inbound message, not per // tool call, so it does not matter whether a tool call happens at all. test("a no-op tool call between nudges does not reset the idle budget", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); // Two content-free terminations spend two of the three nudges. @@ -363,7 +363,7 @@ describe("open-task termination guard", () => { }); test("a new user message resets the idle budget for the next turn", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); for (let i = 0; i < 3; i++) { @@ -388,7 +388,7 @@ describe("open-task termination guard", () => { }); test("a successful tool call between declines does not reset the declined budget", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); // Spend both of the declined-path nudges, with a successful tool result @@ -420,7 +420,7 @@ describe("open-task termination guard", () => { }); test("a declined tool with no open tasks surfaces the decline immediately", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); const actions = actionsArray( await director.decide(makeToolErrorEvent("c", declined), mockState, mockCapabilities), ); @@ -459,7 +459,7 @@ describe("chatDirector compaction", () => { test("schedules idle compaction after an over-threshold text-only reply", async () => { let continuations = 0; const director = createChatDirector("", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, requestContinuation: () => { continuations++; }, @@ -494,8 +494,8 @@ describe("chatDirector compaction", () => { test("idle empty compact makes the post-compact estimate authoritative without inferring", async () => { const director = createChatDirector("", [], { - onTasksChange: () => {}, - requestContinuation: () => {}, + onTasksChange: () => undefined, + requestContinuation: () => undefined, }); const largeTurns = Array.from( { length: compactorNoOpFloor(COMPACTOR_KEEP_RECENT_TURNS) + 1 }, @@ -559,8 +559,8 @@ describe("chatDirector compaction", () => { function chatDirectorWithContinuation(systemPrompt: string, onContinuation?: () => void) { return createChatDirector(systemPrompt, [], { - onTasksChange: () => {}, - requestContinuation: onContinuation ?? (() => {}), + onTasksChange: () => undefined, + requestContinuation: onContinuation ?? (() => undefined), }); } @@ -716,7 +716,7 @@ describe("chatDirector LSP auto-activation", () => { test("reading a code file activates the lsp tool on success", async () => { const activated: string[][] = []; const director = createChatDirector("", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, onActivateTools: (names: string[]) => activated.push(names), }); await director.decide( @@ -731,7 +731,7 @@ describe("chatDirector LSP auto-activation", () => { test("editing a code file activates lsp", async () => { const activated: string[][] = []; const director = createChatDirector("", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, onActivateTools: (names: string[]) => activated.push(names), }); await director.decide( @@ -746,7 +746,7 @@ describe("chatDirector LSP auto-activation", () => { test("a non-code file does not activate lsp", async () => { const activated: string[][] = []; const director = createChatDirector("", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, onActivateTools: (names: string[]) => activated.push(names), }); await director.decide( @@ -761,7 +761,7 @@ describe("chatDirector LSP auto-activation", () => { test("a failed read does not activate lsp", async () => { const activated: string[][] = []; const director = createChatDirector("", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, onActivateTools: (names: string[]) => activated.push(names), }); await director.decide( @@ -808,7 +808,7 @@ describe("updateToolDefinitions rewrites infer tools", () => { }; test("a tool registered after construction is advertised on the next inference", async () => { - const director = createChatDirector("base-prompt", [], { onTasksChange: () => {} }); + const director = createChatDirector("base-prompt", [], { onTasksChange: () => undefined }); director.updateToolDefinitions([lateTool]); const result = await director.decide( @@ -826,7 +826,7 @@ describe("updateToolDefinitions rewrites infer tools", () => { // The provider cache is a prefix cache keyed on the tools array; a tool_search // between turns must not reshape it. test("wire tools are byte-identical across a turn that ran tool_search", async () => { - const director = createChatDirector("base-prompt", [lateTool], { onTasksChange: () => {} }); + const director = createChatDirector("base-prompt", [lateTool], { onTasksChange: () => undefined }); const before = await firstInferTools(director, makeMessageReceivedEvent("do work")); @@ -847,7 +847,7 @@ describe("updateToolDefinitions rewrites infer tools", () => { // submit_output is always on the wire so a workflow going active never grows // the array and busts the provider cache prefix. test("submit_output is advertised even with no active workflow", async () => { - const director = createChatDirector("base-prompt", [], { onTasksChange: () => {} }); + const director = createChatDirector("base-prompt", [], { onTasksChange: () => undefined }); director.updateToolDefinitions([lateTool]); const result = await director.decide( @@ -892,7 +892,7 @@ describe("updateToolDefinitions rewrites infer tools", () => { const director = createChatDirector( "base-prompt", computeAdvertised(toolset.dynamicRunner.currentDefinitions()), - { onTasksChange: () => {} }, + { onTasksChange: () => undefined }, ); // Before discovery: the MCP tool is registered (dispatchable) but not wired. @@ -933,7 +933,7 @@ describe("updateToolDefinitions rewrites infer tools", () => { const classifier = async (_msg: string, _meta: SessionMetadata) => ({ kind: "new_task" as const, reason: "pivot" }) as TaskBoundary; const director = createChatDirector("base-prompt", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, taskClassifier: classifier, }); director.updateToolDefinitions([lateTool]); @@ -1101,7 +1101,7 @@ describe("transient nudges", () => { }) as unknown as ReactorInboundEvent; test("open-task nudge uses ephemeralTurns and keeps the stable system prompt", async () => { - const director = createChatDirector("stable-base", [], { onTasksChange: () => {} }); + const director = createChatDirector("stable-base", [], { onTasksChange: () => undefined }); await director.decide(manageTasksEvent("doing"), mockState, mockCapabilities); const actions = actionsArray(await director.decide(textTurn(), mockState, mockCapabilities)); const infer = actions.find((a) => a.type === "infer"); @@ -1148,7 +1148,7 @@ describe("chatDirector spacer echo", () => { test("spacer-only assistant reply is not a finished turn", async () => { for (const text of [LEGACY_COMPACT_SPACER_TEXT, COMPACT_SPACER_TEXT]) { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); const actions = actionsArray( await director.decide(spacerInferenceDone(text), mockState, mockCapabilities), ); @@ -1162,7 +1162,7 @@ describe("chatDirector spacer echo", () => { test("spacer-echo does not arm idle compact, including after the nudge cap", async () => { let continuations = 0; const director = createChatDirector("base", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, requestContinuation: () => { continuations++; }, @@ -1189,7 +1189,7 @@ describe("chatDirector spacer echo", () => { }); test("echo-nudge cap is two then empty settle, and resets on message.received", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); for (let i = 0; i < 2; i++) { const nudged = actionsArray( await director.decide( @@ -1221,7 +1221,7 @@ describe("chatDirector spacer echo", () => { }); test("after echo-cap with open tasks, falls through to open-task rails", async () => { - const director = createChatDirector("base", [], { onTasksChange: () => {} }); + const director = createChatDirector("base", [], { onTasksChange: () => undefined }); await director.decide( makeInferenceDoneEvent([ { diff --git a/src/exec/runner.ts b/src/exec/runner.ts index bedc98eb1..c459478e2 100644 --- a/src/exec/runner.ts +++ b/src/exec/runner.ts @@ -474,6 +474,7 @@ export async function runExec(config: Config): Promise { const overlay = resolveExecDirectorOverlay(config.director); const workflowHostHolder: { instance?: WorkflowHost } = {}; + const subAgentSettings = config.settings; const agentToolset = await createAgentToolset({ cwd: config.cwd, @@ -526,7 +527,7 @@ export async function runExec(config: Config): Promise { sessions: fleetSessions, getWorkdirBase: () => sessionDir(config.cwd, sessionId), onProgress: () => undefined, - ...(config.settings !== undefined ? { settings: () => config.settings! } : {}), + ...(subAgentSettings !== undefined ? { settings: () => subAgentSettings } : {}), catalog: () => config.providers, profiles: () => liveAgentProfiles, }, @@ -975,7 +976,10 @@ async function promptPermission( if (!Number.isInteger(n) || n < 1 || n > scopes.length) { return { allow: false }; } - const chosen = scopes[n - 1]!; + const chosen = scopes[n - 1]; + if (chosen === undefined) { + return { allow: false }; + } return { allow: true, ...(chosen.pattern !== null ? { persist: chosen } : {}), diff --git a/src/extensions/skills.ts b/src/extensions/skills.ts index a90d946c8..2766a57d0 100644 --- a/src/extensions/skills.ts +++ b/src/extensions/skills.ts @@ -65,7 +65,13 @@ function parseSkillFrontmatter(raw: string): { for (const line of block.split("\n")) { const trimmed = line.trim(); const match = /^(name|description):\s*(.+)$/.exec(trimmed); - if (match) out[match[1] as "name" | "description"] = match[2]!.trim(); + if (match) { + const key = match[1]; + const value = match[2]; + if ((key === "name" || key === "description") && value !== undefined) { + out[key] = value.trim(); + } + } if (/^disable-model-invocation:\s*true\s*$/.test(trimmed)) { out.disableModelInvocation = true; } diff --git a/src/mcp/client-auth-policy.test.ts b/src/mcp/client-auth-policy.test.ts index 8c2e93ccc..6e718d7f6 100644 --- a/src/mcp/client-auth-policy.test.ts +++ b/src/mcp/client-auth-policy.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { beforeEach, describe, expect, test } from "bun:test"; import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; import { withMockedModule } from "../../tests/helpers/mock-module.js"; @@ -302,7 +303,7 @@ describe("HTTP MCP auth policy", () => { } ).fetch; expect(fetchFn).toBeTypeOf("function"); - await expect(fetchFn!("https://auth.test/token")).rejects.toThrow(); + await expect(defined(fetchFn)("https://auth.test/token")).rejects.toThrow(); }); test("ordinary HTTP creates endpoint-scoped OAuth and passes it to transport", async () => { diff --git a/src/mcp/client-auth-reauth-cap.test.ts b/src/mcp/client-auth-reauth-cap.test.ts index 611f38234..6841add5a 100644 --- a/src/mcp/client-auth-reauth-cap.test.ts +++ b/src/mcp/client-auth-reauth-cap.test.ts @@ -161,7 +161,9 @@ await withMockedModule( await waitForOptionalGate(retryGate, lastRequestSignal); return { content: [] }; } - async close(): Promise {} + async close(): Promise { + return undefined; + } }, }), ); diff --git a/src/mcp/plugin.test.ts b/src/mcp/plugin.test.ts index 6a7e19f82..dbb9714dc 100644 --- a/src/mcp/plugin.test.ts +++ b/src/mcp/plugin.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { mcpClientToAgentTools } from "./plugin.js"; import { createPermissionGate } from "../permission/gate.js"; @@ -104,7 +105,7 @@ describe("mcpClientToAgentTools", () => { const entry = store.blobs.get(key); expect(entry).toBeDefined(); expect(entry?.contentType).toBe("application/json"); - expect(new TextDecoder().decode(entry!.bytes)).toBe(pretty); + expect(new TextDecoder().decode(defined(entry).bytes)).toBe(pretty); const uri = `tool-output:///${key}`; const abs = toolOutputAbsolutePath(contextDir, key, "application/json"); @@ -137,7 +138,7 @@ describe("mcpClientToAgentTools", () => { ); const spilled = new TextDecoder().decode( - store.blobs.get(spillBlobKey("c-mcp-json-secret"))!.bytes, + defined(store.blobs.get(spillBlobKey("c-mcp-json-secret"))).bytes, ); expect(result.content).toContain(CREDENTIAL_REDACTION); expect(result.content).not.toContain("sk-live-"); @@ -166,7 +167,7 @@ describe("mcpClientToAgentTools", () => { const key = spillBlobKey("c-mcp-txt"); const entry = store.blobs.get(key); expect(entry?.contentType).toBe("text/plain"); - expect(new TextDecoder().decode(entry!.bytes)).toBe(huge); + expect(new TextDecoder().decode(defined(entry).bytes)).toBe(huge); expect(result.content).toContain(`tool-output:///${key}`); expect(result.content).toContain(toolOutputAbsolutePath(contextDir, key, "text/plain")); }); diff --git a/src/mcp/tool-name.ts b/src/mcp/tool-name.ts index 03f57b70c..36f1239d5 100644 --- a/src/mcp/tool-name.ts +++ b/src/mcp/tool-name.ts @@ -29,7 +29,8 @@ export function parseMcpToolName(name: string): { server: string; tool: string } } function titleCase(word: string): string { - return word.length === 0 ? word : word[0]!.toUpperCase() + word.slice(1); + const first = word[0]; + return first === undefined ? word : first.toUpperCase() + word.slice(1); } // Some servers suffix (or prefix) every tool name with their own name, a @@ -38,9 +39,11 @@ function titleCase(word: string): string { // that matches the server, case-insensitively, so it is not said twice. export function mcpToolWords(server: string, tool: string): string[] { const words = tool.split("_").filter((word) => word.length > 0); - if (words.length > 1 && words[words.length - 1]!.toLowerCase() === server.toLowerCase()) { + const last = words[words.length - 1]; + const first = words[0]; + if (words.length > 1 && last !== undefined && last.toLowerCase() === server.toLowerCase()) { words.pop(); - } else if (words.length > 1 && words[0]!.toLowerCase() === server.toLowerCase()) { + } else if (words.length > 1 && first !== undefined && first.toLowerCase() === server.toLowerCase()) { words.shift(); } return words; diff --git a/src/mcp/tool-permissions.ts b/src/mcp/tool-permissions.ts index dd7fa500f..f06f480bb 100644 --- a/src/mcp/tool-permissions.ts +++ b/src/mcp/tool-permissions.ts @@ -52,8 +52,8 @@ export function tierFromMcpTool( serverName: string, toolName: string, ): Tier { - if (hasAnnotationHints(annotations)) { - return annotations!.readOnlyHint === true ? "allow" : "ask"; + if (annotations !== undefined && hasAnnotationHints(annotations)) { + return annotations.readOnlyHint === true ? "allow" : "ask"; } return isReadOnlyMcpTool(mcpToolName(serverName, toolName)) ? "allow" : "ask"; } diff --git a/src/perf/assert-spans.test.ts b/src/perf/assert-spans.test.ts index 00bad041b..4a7e6f729 100644 --- a/src/perf/assert-spans.test.ts +++ b/src/perf/assert-spans.test.ts @@ -16,6 +16,7 @@ * - full observer pipeline → snapshot → rollup → assertions */ +import { defined } from "../../tests/helpers/defined.js"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import type { ReactorEmittedEvent } from "@intx/inference"; import { @@ -155,8 +156,8 @@ describe("assertLessThan", () => { describe("assertTurnHasInferenceAndTools", () => { test("passes on multi-tool golden rollup", () => { const turns = rollupByTurn(multiToolTurnFixture()); - assertTurnHasInferenceAndTools(turns[0]!); - assertTurnHasInferenceAndTools(turns[0]!, { minToolCount: 2 }); + assertTurnHasInferenceAndTools(defined(turns[0])); + assertTurnHasInferenceAndTools(defined(turns[0]), { minToolCount: 2 }); }); test("throws when inferenceNs is not positive", () => { @@ -188,7 +189,7 @@ describe("assertTurnHasInferenceAndTools", () => { test("fails when tools are filtered out of the golden fixture", () => { const spans: PerfSpan[] = multiToolTurnFixture().filter((s) => s.name !== "tool"); const turns = rollupByTurn(spans); - expect(() => assertTurnHasInferenceAndTools(turns[0]!)).toThrow(/toolCount/); + expect(() => assertTurnHasInferenceAndTools(defined(turns[0]))).toThrow(/toolCount/); }); }); @@ -209,7 +210,7 @@ describe("golden multi-tool turn fixture", () => { }); test("TTFT is strictly less than stream on the golden fixture", () => { - const turn = rollupByTurn(multiToolTurnFixture())[0]!; + const turn = defined(rollupByTurn(multiToolTurnFixture())[0]); assertLessThan(turn.ttftNs, turn.streamNs, "ttft vs stream"); }); }); @@ -246,8 +247,8 @@ describe("observer pipeline → snapshot → rollup → assertions", () => { const turns = rollupByTurn(spans); expect(turns).toHaveLength(1); - assertTurnHasInferenceAndTools(turns[0]!, { minToolCount: 2 }); - expect(turns[0]!.toolCount).toBe(2); + assertTurnHasInferenceAndTools(defined(turns[0]), { minToolCount: 2 }); + expect(defined(turns[0]).toolCount).toBe(2); // Live clock: duration magnitudes are non-deterministic under sync hrtime // (ttftNs can exceed streamNs). Assert wall ordering instead of a no-op @@ -256,8 +257,11 @@ describe("observer pipeline → snapshot → rollup → assertions", () => { const stream = spans.find((s) => s.name === "inference.stream"); expect(ttft?.endNs).toBeDefined(); expect(stream?.startNs).toBeDefined(); - if (ttft!.endNs !== undefined && stream !== undefined) { - expect(ttft!.endNs <= stream.startNs).toBe(true); + if (stream !== undefined) { + const ttftEndNs = defined(ttft, "ttft").endNs; + if (ttftEndNs !== undefined) { + expect(ttftEndNs <= stream.startNs).toBe(true); + } } const phases = rollupByPhase(spans); diff --git a/src/perf/attribution-report.test.ts b/src/perf/attribution-report.test.ts index 46e27e040..9d0a0614e 100644 --- a/src/perf/attribution-report.test.ts +++ b/src/perf/attribution-report.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { attributionFromDump, @@ -74,7 +75,7 @@ describe("attributionFromSpans — multi-tool golden fixture", () => { test("per-turn row mirrors session for single-turn fixture", () => { const report = attributionFromSpans(multiToolTurnFixture()); expect(report.turns).toHaveLength(1); - const t = report.turns[0]!; + const t = defined(report.turns[0]); expect(t.turnId).toBe("t1"); expect(t.turnNs).toBe(5000); expect(t.open).toBe(false); @@ -246,7 +247,7 @@ describe("attributionFromSpans — subagent + transport", () => { expect(report.session.inference.ttftNs).toBe(400 + 500); expect(report.session.inference.streamNs).toBe(1600 + 2000); - const turnShareSum = report.turns[0]!.categories.reduce((a, c) => a + c.share, 0); + const turnShareSum = defined(report.turns[0]).categories.reduce((a, c) => a + c.share, 0); expect(turnShareSum).toBeCloseTo(1, 10); }); }); @@ -290,12 +291,12 @@ describe("attributionFromSpans — open (stall) turns", () => { expect(report.session.turnCount).toBe(1); // wall = maxEnd(3100) - start(100) = 3000 expect(report.session.wallNs).toBe(3000); - expect(report.turns[0]!.open).toBe(true); - expect(report.turns[0]!.turnNs).toBe(3000); + expect(defined(report.turns[0]).open).toBe(true); + expect(defined(report.turns[0]).turnNs).toBe(3000); expect(report.session.open).toBe(true); // Still-running: turn + open stream (completed inference/tool are not listed) expect(report.session.openPhases).toEqual(["inference.stream", "turn"]); - expect(report.turns[0]!.openPhases).toEqual(["inference.stream", "turn"]); + expect(defined(report.turns[0]).openPhases).toEqual(["inference.stream", "turn"]); expect(categoryShare(report.session.categories, "inference").ns).toBe(2000); expect(categoryShare(report.session.categories, "tools").ns).toBe(1000); @@ -305,7 +306,7 @@ describe("attributionFromSpans — open (stall) turns", () => { const shareSum = report.session.categories.reduce((a, c) => a + c.share, 0); expect(shareSum).toBeCloseTo(1, 10); - const turnShareSum = report.turns[0]!.categories.reduce((a, c) => a + c.share, 0); + const turnShareSum = defined(report.turns[0]).categories.reduce((a, c) => a + c.share, 0); expect(turnShareSum).toBeCloseTo(1, 10); }); @@ -364,7 +365,7 @@ describe("attributionFromSpans — open (stall) turns", () => { const shareSum = report.session.categories.reduce((a, c) => a + c.share, 0); expect(shareSum).toBeCloseTo(1, 10); - const openTurn = report.turns.find((t) => t.turnId === "t1")!; + const openTurn = defined(report.turns.find((t) => t.turnId === "t1")); expect(openTurn.open).toBe(true); expect(openTurn.turnNs).toBe(2000); const openShareSum = openTurn.categories.reduce((a, c) => a + c.share, 0); @@ -383,11 +384,11 @@ describe("attributionFromSpans — open (stall) turns", () => { ]; const report = attributionFromSpans(spans); expect(report.session.wallNs).toBe(0); - expect(report.turns[0]!.open).toBe(true); - expect(report.turns[0]!.turnNs).toBe(0); + expect(defined(report.turns[0]).open).toBe(true); + expect(defined(report.turns[0]).turnNs).toBe(0); expect(report.session.open).toBe(true); expect(report.session.openPhases).toEqual(["inference", "turn"]); - expect(report.turns[0]!.openPhases).toEqual(["inference", "turn"]); + expect(defined(report.turns[0]).openPhases).toEqual(["inference", "turn"]); for (const c of report.session.categories) { expect(c.share).toBe(0); expect(c.ns).toBe(0); @@ -412,8 +413,8 @@ describe("dump round-trip", () => { const serialized = multiToolTurnFixture().map(serializeSpan); const spans = spansFromDumpJson(serialized); expect(spans).toHaveLength(7); - expect(spans[0]!.startNs).toBe(0n); - expect(deserializeDumpSpan(serialized[0]!).id).toBe("t1"); + expect(defined(spans[0]).startNs).toBe(0n); + expect(deserializeDumpSpan(defined(serialized[0])).id).toBe("t1"); }); test("attributionFromDump rejects unsupported DUMP_VERSION", () => { diff --git a/src/perf/index.test.ts b/src/perf/index.test.ts index 48b62d83b..c9f8c3732 100644 --- a/src/perf/index.test.ts +++ b/src/perf/index.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { OPEN_SPAN_CAPACITY, RING_CAPACITY, @@ -29,10 +30,10 @@ describe("start / end / mark", () => { const spans = snapshot(); expect(spans).toHaveLength(1); - const span = spans[0]!; + const span = defined(spans[0]); expect(span.name).toBe("inference"); expect(span.endNs).toBeDefined(); - expect(span.endNs! >= span.startNs).toBe(true); + expect(defined(span.endNs) >= span.startNs).toBe(true); }); test("nests via parentId", () => { @@ -43,8 +44,8 @@ describe("start / end / mark", () => { const spans = snapshot(); expect(spans).toHaveLength(2); - const inference = spans.find((s) => s.name === "inference")!; - const turn = spans.find((s) => s.name === "turn")!; + const inference = defined(spans.find((s) => s.name === "inference")); + const turn = defined(spans.find((s) => s.name === "turn")); expect(inference.parentId).toBe(turnId); expect(turn.parentId).toBeUndefined(); }); @@ -55,8 +56,8 @@ describe("start / end / mark", () => { const spans = snapshot(); expect(spans).toHaveLength(1); - expect(spans[0]!.startNs).toBe(spans[0]!.endNs!); - expect(spans[0]!.tags).toEqual({ transport: "http_sse" }); + expect(defined(spans[0]).startNs).toBe(defined(defined(spans[0]).endNs)); + expect(defined(spans[0]).tags).toEqual({ transport: "http_sse" }); }); test("mark accepts optional parentId like start", () => { @@ -65,7 +66,7 @@ describe("start / end / mark", () => { end(turnId); const spans = snapshot(); - const marked = spans.find((s) => s.name === "adapter.transport")!; + const marked = defined(spans.find((s) => s.name === "adapter.transport")); expect(marked.parentId).toBe(turnId); expect(marked.tags).toEqual({ transport: "ws" }); }); @@ -74,8 +75,8 @@ describe("start / end / mark", () => { const id = start("session"); const spans = snapshot(); expect(spans).toHaveLength(1); - expect(spans[0]!.id).toBe(id); - expect(spans[0]!.endNs).toBeUndefined(); + expect(defined(spans[0]).id).toBe(id); + expect(defined(spans[0]).endNs).toBeUndefined(); }); test("unknown span names are ignored", () => { @@ -107,7 +108,7 @@ describe("start / end / mark", () => { test("end merges sanitized tags onto the span", () => { const id = start("tool", { tags: { tool_id: "t1" } }); end(id, { count: 3, prompt: "secret" }); - const span = snapshot()[0]!; + const span = defined(snapshot()[0]); expect(span.tags).toEqual({ tool_id: "t1", count: 3 }); }); }); @@ -116,7 +117,7 @@ describe("parentId privacy fence", () => { test("strips path-like parentId", () => { const id = start("inference", { parentId: "/Users/me/secret/repo/src/main.ts" }); end(id); - expect(snapshot()[0]!.parentId).toBeUndefined(); + expect(defined(snapshot()[0]).parentId).toBeUndefined(); }); test("strips free-text and path-separator parentIds", () => { @@ -144,8 +145,8 @@ describe("parentId privacy fence", () => { end(orphan); const spans = snapshot(); - expect(spans.find((s) => s.name === "inference")!.parentId).toBe(parent); - expect(spans.find((s) => s.name === "tool")!.parentId).toBe("parent1"); + expect(defined(spans.find((s) => s.name === "inference")).parentId).toBe(parent); + expect(defined(spans.find((s) => s.name === "tool")).parentId).toBe("parent1"); }); test("accepts parentId of a completed (ring) span", () => { @@ -153,7 +154,7 @@ describe("parentId privacy fence", () => { end(parent); const child = start("inference", { parentId: parent }); end(child); - expect(snapshot().find((s) => s.name === "inference")!.parentId).toBe(parent); + expect(defined(snapshot().find((s) => s.name === "inference")).parentId).toBe(parent); }); }); @@ -164,10 +165,10 @@ describe("snapshot immutability", () => { const first = snapshot(); expect(first).toHaveLength(1); - first[0]!.name = "session"; - first[0]!.tags!.tool_id = "POISON"; - first[0]!.tags!.count = 999; - first[0]!.parentId = "injected"; + defined(first[0]).name = "session"; + defined(defined(first[0]).tags).tool_id = "POISON"; + defined(defined(first[0]).tags).count = 999; + defined(first[0]).parentId = "injected"; first.push({ id: "fake", name: "session", @@ -177,20 +178,20 @@ describe("snapshot immutability", () => { const second = snapshot(); expect(second).toHaveLength(1); - expect(second[0]!.name).toBe("tool"); - expect(second[0]!.tags).toEqual({ tool_id: "t1", count: 1 }); - expect(second[0]!.parentId).toBeUndefined(); + expect(defined(second[0]).name).toBe("tool"); + expect(defined(second[0]).tags).toEqual({ tool_id: "t1", count: 1 }); + expect(defined(second[0]).parentId).toBeUndefined(); }); test("mutating an open-span snapshot does not poison open state", () => { start("session", { tags: { provider_id: "openai" } }); const first = snapshot(); - first[0]!.tags!.provider_id = "POISON"; - first[0]!.endNs = 1n; + defined(defined(first[0]).tags).provider_id = "POISON"; + defined(first[0]).endNs = 1n; const second = snapshot(); - expect(second[0]!.tags).toEqual({ provider_id: "openai" }); - expect(second[0]!.endNs).toBeUndefined(); + expect(defined(second[0]).tags).toEqual({ provider_id: "openai" }); + expect(defined(second[0]).endNs).toBeUndefined(); }); }); @@ -205,8 +206,8 @@ describe("ring overflow", () => { expect(spans).toHaveLength(RING_CAPACITY); // Oldest surviving should be count = 10 (0..9 dropped). - const first = spans[0]!; - const last = spans[spans.length - 1]!; + const first = defined(spans[0]); + const last = defined(spans[spans.length - 1]); expect(first.tags?.count).toBe(10); expect(last.tags?.count).toBe(RING_CAPACITY + 9); }); @@ -235,11 +236,11 @@ describe("open span capacity", () => { expect(counts[counts.length - 1]).toBe(OPEN_SPAN_CAPACITY + 4); // Ending an evicted id is a no-op; ending a survivor still works. - end(ids[0]!); - end(ids[ids.length - 1]!); + end(defined(ids[0])); + end(defined(ids[ids.length - 1])); const completed = snapshot().filter((s) => s.endNs !== undefined); expect(completed).toHaveLength(1); - expect(completed[0]!.tags?.count).toBe(OPEN_SPAN_CAPACITY + 4); + expect(defined(completed[0]).tags?.count).toBe(OPEN_SPAN_CAPACITY + 4); }); }); @@ -352,7 +353,7 @@ describe("snapshot shape", () => { }); end(id); - const span: PerfSpan = snapshot()[0]!; + const span: PerfSpan = defined(snapshot()[0]); expect(Object.keys(span).sort()).toEqual( ["endNs", "id", "name", "parentId", "startNs", "tags"].sort(), ); diff --git a/src/perf/otel-sink.test.ts b/src/perf/otel-sink.test.ts index 62c3c5ef7..7dc5748ff 100644 --- a/src/perf/otel-sink.test.ts +++ b/src/perf/otel-sink.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import type { Settings } from "../config/settings.js"; @@ -121,11 +122,11 @@ describe("buildOtlpPayload", () => { traceId: "a".repeat(32), }); - const otlpSpans = payload.resourceSpans[0]!.scopeSpans[0]!.spans; + const otlpSpans = defined(defined(payload.resourceSpans[0]).scopeSpans[0]).spans; expect(otlpSpans).toHaveLength(2); - const turn = otlpSpans.find((s) => s.name === "turn")!; - const inf = otlpSpans.find((s) => s.name === "inference")!; + const turn = defined(otlpSpans.find((s) => s.name === "turn")); + const inf = defined(otlpSpans.find((s) => s.name === "inference")); expect(turn.traceId).toBe("a".repeat(32)); expect(turn.spanId).toBe(otelSpanId(turnId)); expect(turn.parentSpanId).toBeUndefined(); @@ -135,7 +136,7 @@ describe("buildOtlpPayload", () => { expect(inf.parentSpanId).toBe(otelSpanId(turnId)); expect(inf.attributes).toEqual([{ key: "model_id", value: { stringValue: "m1" } }]); - const resource = payload.resourceSpans[0]!.resource.attributes; + const resource = defined(payload.resourceSpans[0]).resource.attributes; expect( resource.some( (a) => @@ -154,7 +155,7 @@ describe("buildOtlpPayload", () => { nowMonoNs: () => 99n, traceId: "b".repeat(32), }); - const span = payload.resourceSpans[0]!.scopeSpans[0]!.spans[0]!; + const span = defined(defined(defined(payload.resourceSpans[0]).scopeSpans[0]).spans[0]); expect(span.startTimeUnixNano).toBe("10"); expect(span.endTimeUnixNano).toBe("99"); }); @@ -179,12 +180,12 @@ describe("flushToOtel", () => { ); expect(calls).toHaveLength(1); - expect(calls[0]!.url).toBe("https://collector.example/v1/traces"); - const headers = calls[0]!.init.headers as Record; + expect(defined(calls[0]).url).toBe("https://collector.example/v1/traces"); + const headers = defined(calls[0]).init.headers as Record; expect(headers["content-type"]).toBe("application/json"); expect(headers.Authorization).toBe("Bearer secret"); - const body = JSON.parse(String(calls[0]!.init.body)) as { + const body = JSON.parse(String(defined(calls[0]).init.body)) as { resourceSpans: unknown[]; }; expect(body.resourceSpans).toHaveLength(1); @@ -289,12 +290,12 @@ describe("flushPerfToOtel", () => { }, ); expect(bodies).toHaveLength(1); - const parsed = JSON.parse(bodies[0]!) as { + const parsed = JSON.parse(defined(bodies[0])) as { resourceSpans: { scopeSpans: { spans: { name: string }[] }[]; }[]; }; - const names = parsed.resourceSpans[0]!.scopeSpans[0]!.spans.map((s) => s.name); + const names = defined(defined(parsed.resourceSpans[0]).scopeSpans[0]).spans.map((s) => s.name); expect(names).toEqual(["tool"]); }); }); diff --git a/src/perf/permission-subagent-spans.test.ts b/src/perf/permission-subagent-spans.test.ts index d46f40998..e9b46e1e7 100644 --- a/src/perf/permission-subagent-spans.test.ts +++ b/src/perf/permission-subagent-spans.test.ts @@ -1,6 +1,7 @@ /** * CL-5170: permission.wait and subagent spans at the ask gate and task fleet. */ +import { defined } from "../../tests/helpers/defined.js"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import type { ReactorEmittedEvent } from "@intx/inference"; import { createPermissionGate } from "../permission/gate.js"; @@ -47,7 +48,7 @@ describe("permission.wait spans", () => { const waits = byName(completed(snapshot()), "permission.wait"); expect(waits).toHaveLength(1); - expect(waits[0]!.tags).toEqual({ tool_id: "run_shell", decision: "allow" }); + expect(defined(waits[0]).tags).toEqual({ tool_id: "run_shell", decision: "allow" }); }); test("records deny decision when operator declines", async () => { @@ -64,8 +65,8 @@ describe("permission.wait spans", () => { const waits = byName(completed(snapshot()), "permission.wait"); expect(waits).toHaveLength(1); - expect(waits[0]!.tags?.decision).toBe("deny"); - expect(waits[0]!.tags?.tool_id).toBe("run_shell"); + expect(defined(waits[0]).tags?.decision).toBe("deny"); + expect(defined(waits[0]).tags?.tool_id).toBe("run_shell"); }); test("permission.wait tags never include free-text reason/prompt — only tool_id + decision", async () => { @@ -89,7 +90,7 @@ describe("permission.wait spans", () => { const waits = byName(completed(snapshot()), "permission.wait"); expect(waits).toHaveLength(1); - const tags = waits[0]!.tags ?? {}; + const tags = defined(waits[0]).tags ?? {}; // Allowlist: only tool_id + decision enums on permission.wait. expect(Object.keys(tags).sort()).toEqual(["decision", "tool_id"]); expect(tags).toEqual({ tool_id: "run_shell", decision: "deny" }); @@ -130,7 +131,7 @@ describe("permission.wait spans", () => { const waits = byName(completed(snapshot()), "permission.wait"); expect(waits).toHaveLength(1); - expect(waits[0]!.tags).toEqual({ tool_id: "write_file", decision: "allow" }); + expect(defined(waits[0]).tags).toEqual({ tool_id: "write_file", decision: "allow" }); }); test("closes permission.wait when requestApproval throws", async () => { @@ -148,10 +149,10 @@ describe("permission.wait spans", () => { const waits = byName(completed(snapshot()), "permission.wait"); expect(waits).toHaveLength(1); - expect(waits[0]!.endNs).toBeDefined(); - expect(waits[0]!.tags?.tool_id).toBe("run_shell"); + expect(defined(waits[0]).endNs).toBeDefined(); + expect(defined(waits[0]).tags?.tool_id).toBe("run_shell"); // No decision tag when approval never returned. - expect(waits[0]!.tags?.decision).toBeUndefined(); + expect(defined(waits[0]).tags?.decision).toBeUndefined(); }); test("clear() nulls process-wide currentTurnId", () => { @@ -208,8 +209,8 @@ describe("permission.wait spans", () => { }); await gate.evaluate(shellCall("curl x")); - const wait = byName(completed(snapshot()), "permission.wait")[0]!; - expect(wait.parentId).toBe(turnId!); + const wait = defined(byName(completed(snapshot()), "permission.wait")[0]); + expect(wait.parentId).toBe(defined(turnId)); obs.reset(); }); diff --git a/src/perf/reactor-spans.test.ts b/src/perf/reactor-spans.test.ts index 2bfdea16a..459647ea3 100644 --- a/src/perf/reactor-spans.test.ts +++ b/src/perf/reactor-spans.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import type { ReactorEmittedEvent } from "@intx/inference"; import { clear, snapshot, type PerfSpan } from "./index.js"; import { createPerfReactorObserver } from "./reactor-spans.js"; @@ -59,10 +60,10 @@ describe("createPerfReactorObserver", () => { expect(ttfts).toHaveLength(1); expect(streams).toHaveLength(1); - const turn = turns[0]!; - const inference = inferences[0]!; - const ttft = ttfts[0]!; - const stream = streams[0]!; + const turn = defined(turns[0]); + const inference = defined(inferences[0]); + const ttft = defined(ttfts[0]); + const stream = defined(streams[0]); expect(turn.parentId).toBeUndefined(); expect(inference.parentId).toBe(turn.id); @@ -70,9 +71,9 @@ describe("createPerfReactorObserver", () => { expect(stream.parentId).toBe(inference.id); // Ordering: ttft ends at/before stream starts; stream ends at/before inference ends. - expect(ttft.endNs! <= stream.startNs).toBe(true); - expect(stream.endNs! <= inference.endNs!).toBe(true); - expect(inference.endNs! <= turn.endNs!).toBe(true); + expect(defined(ttft.endNs) <= stream.startNs).toBe(true); + expect(defined(stream.endNs) <= defined(inference.endNs)).toBe(true); + expect(defined(inference.endNs) <= defined(turn.endNs)).toBe(true); }); test("tool spans nest under turn after inference.done with tool_calls", () => { @@ -87,13 +88,13 @@ describe("createPerfReactorObserver", () => { obs.observe(event("tool.done", { result: { callId: "call-1", content: "ok" } })); const spans = completed(snapshot()); - const turn = byName(spans, "turn")[0]!; - const inference = byName(spans, "inference")[0]!; + const turn = defined(byName(spans, "turn")[0]); + const inference = defined(byName(spans, "inference")[0]); const tools = byName(spans, "tool"); expect(tools).toHaveLength(1); - expect(tools[0]!.parentId).toBe(turn.id); - expect(tools[0]!.tags?.tool_id).toBe("call-1"); + expect(defined(tools[0]).parentId).toBe(turn.id); + expect(defined(tools[0]).tags?.tool_id).toBe("call-1"); expect(inference.parentId).toBe(turn.id); expect(turn.endNs).toBeDefined(); }); @@ -114,8 +115,8 @@ describe("createPerfReactorObserver", () => { expect(turns).toHaveLength(2); expect(inferences).toHaveLength(2); expect(turns.every((t) => t.parentId === undefined)).toBe(true); - expect(inferences[0]!.parentId).toBe(turns[0]!.id); - expect(inferences[1]!.parentId).toBe(turns[1]!.id); + expect(defined(inferences[0]).parentId).toBe(defined(turns[0]).id); + expect(defined(inferences[1]).parentId).toBe(defined(turns[1]).id); }); test("inference without stream deltas has turn + inference only (no stream)", () => { @@ -159,8 +160,8 @@ describe("createPerfReactorObserver", () => { const tools = byName(completed(snapshot()), "tool"); expect(tools).toHaveLength(1); - expect(tools[0]!.tags?.tool_id).toBe("blocked-1"); - expect(byName(completed(snapshot()), "turn")[0]!.endNs).toBeDefined(); + expect(defined(tools[0]).tags?.tool_id).toBe("blocked-1"); + expect(defined(byName(completed(snapshot()), "turn")[0]).endNs).toBeDefined(); }); test("reset closes open spans and clears state", () => { @@ -196,10 +197,10 @@ describe("createPerfReactorObserver", () => { const inferences = byName(completed(spans), "inference"); expect(turns).toHaveLength(2); expect(inferences).toHaveLength(2); - expect(inferences[0]!.parentId).toBe(turns[0]!.id); - expect(inferences[1]!.parentId).toBe(turns[1]!.id); + expect(defined(inferences[0]).parentId).toBe(defined(turns[0]).id); + expect(defined(inferences[1]).parentId).toBe(defined(turns[1]).id); // First turn abandoned before second opened — not nested. - expect(turns[0]!.endNs! <= turns[1]!.startNs).toBe(true); + expect(defined(defined(turns[0]).endNs) <= defined(turns[1]).startNs).toBe(true); }); test("abandon mid-tool then new start closes open tools and prior turn", () => { @@ -223,13 +224,13 @@ describe("createPerfReactorObserver", () => { expect(turns).toHaveLength(2); expect(tools).toHaveLength(1); - expect(tools[0]!.parentId).toBe(turns[0]!.id); + expect(defined(tools[0]).parentId).toBe(defined(turns[0]).id); expect(inferences).toHaveLength(2); - expect(inferences[0]!.parentId).toBe(turns[0]!.id); - expect(inferences[1]!.parentId).toBe(turns[1]!.id); + expect(defined(inferences[0]).parentId).toBe(defined(turns[0]).id); + expect(defined(inferences[1]).parentId).toBe(defined(turns[1]).id); // Second inference must not nest under the abandoned turn. - expect(inferences[1]!.parentId).not.toBe(turns[0]!.id); - expect(turns[0]!.endNs! <= turns[1]!.startNs).toBe(true); + expect(defined(inferences[1]).parentId).not.toBe(defined(turns[0]).id); + expect(defined(defined(turns[0]).endNs) <= defined(turns[1]).startNs).toBe(true); }); test("inference.error mid-turn then new start leaves no open spans", () => { @@ -249,7 +250,7 @@ describe("createPerfReactorObserver", () => { expect(spans.every((s) => s.endNs !== undefined)).toBe(true); const turns = byName(completed(spans), "turn"); expect(turns).toHaveLength(2); - expect(byName(completed(spans), "inference")[1]!.parentId).toBe(turns[1]!.id); + expect(defined(byName(completed(spans), "inference")[1]).parentId).toBe(defined(turns[1]).id); }); }); @@ -259,7 +260,7 @@ describe("turn collector durationMs unchanged with perf observer", () => { // inference.start re-stamps it, then completePending reads finish. const times = [1_000, 1_000, 1_250]; let i = 0; - const now = (): number => times[Math.min(i++, times.length - 1)]!; + const now = (): number => defined(times[Math.min(i++, times.length - 1)]); const completedTurns: { durationMs: number }[] = []; const collector = createTurnContextCollector((ctx) => { @@ -278,7 +279,7 @@ describe("turn collector durationMs unchanged with perf observer", () => { feed(inferenceDone()); expect(completedTurns).toHaveLength(1); - expect(completedTurns[0]!.durationMs).toBe(250); + expect(defined(completedTurns[0]).durationMs).toBe(250); expect(collector.getTurnCount()).toBe(1); // Perf spans still present and nested. @@ -292,7 +293,7 @@ describe("turn collector durationMs unchanged with perf observer", () => { // completePending reads finish. const times = [5_000, 5_000, 5_400]; let i = 0; - const now = (): number => times[Math.min(i++, times.length - 1)]!; + const now = (): number => defined(times[Math.min(i++, times.length - 1)]); const completedTurns: { durationMs: number }[] = []; const collector = createTurnContextCollector((ctx) => { @@ -313,10 +314,10 @@ describe("turn collector durationMs unchanged with perf observer", () => { feed(event("tool.done", { result: { callId: "c1", content: "ok" } })); expect(completedTurns).toHaveLength(1); - expect(completedTurns[0]!.durationMs).toBe(400); + expect(defined(completedTurns[0]).durationMs).toBe(400); const spans = completed(snapshot()); expect(byName(spans, "tool")).toHaveLength(1); - expect(byName(spans, "tool")[0]!.parentId).toBe(byName(spans, "turn")[0]!.id); + expect(defined(byName(spans, "tool")[0]).parentId).toBe(defined(byName(spans, "turn")[0]).id); }); }); diff --git a/src/perf/rollup.test.ts b/src/perf/rollup.test.ts index beb31b950..06bd9a510 100644 --- a/src/perf/rollup.test.ts +++ b/src/perf/rollup.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -134,7 +135,7 @@ describe("rollupByPhase", () => { span({ id: "a", name: "tool", startNs: 0n, endNs: 100n }), span({ id: "b", name: "tool", startNs: 0n }), // open ]; - const tool = rollupByPhase(spans).find((p) => p.name === "tool")!; + const tool = defined(rollupByPhase(spans).find((p) => p.name === "tool")); expect(tool.count).toBe(2); expect(tool.openCount).toBe(1); expect(tool.totalNs).toBe(100); @@ -219,17 +220,17 @@ describe("rollupByTurn", () => { const turns = rollupByTurn(spans); expect(turns).toHaveLength(2); - expect(turns[0]!.turnId).toBe("t1"); - expect(turns[0]!.inferenceNs).toBe(500); - expect(turns[0]!.ttftNs).toBe(100); - expect(turns[0]!.streamNs).toBe(400); - expect(turns[0]!.toolCount).toBe(0); - expect(turns[1]!.turnId).toBe("t2"); - expect(turns[1]!.inferenceNs).toBe(1000); - expect(turns[1]!.ttftNs).toBe(200); - expect(turns[1]!.streamNs).toBe(800); - expect(turns[1]!.toolNs).toBe(300); - expect(turns[1]!.toolCount).toBe(1); + expect(defined(turns[0]).turnId).toBe("t1"); + expect(defined(turns[0]).inferenceNs).toBe(500); + expect(defined(turns[0]).ttftNs).toBe(100); + expect(defined(turns[0]).streamNs).toBe(400); + expect(defined(turns[0]).toolCount).toBe(0); + expect(defined(turns[1]).turnId).toBe("t2"); + expect(defined(turns[1]).inferenceNs).toBe(1000); + expect(defined(turns[1]).ttftNs).toBe(200); + expect(defined(turns[1]).streamNs).toBe(800); + expect(defined(turns[1]).toolNs).toBe(300); + expect(defined(turns[1]).toolCount).toBe(1); }); test("open turn is flagged and turnNs is 0", () => { @@ -473,7 +474,7 @@ describe("edge: ring eviction and open spans in snapshot", () => { expect(spans).toHaveLength(RING_CAPACITY); const phases = rollupByPhase(spans); - const tool = phases.find((p) => p.name === "tool")!; + const tool = defined(phases.find((p) => p.name === "tool")); expect(tool.count).toBe(RING_CAPACITY); // Durations of mark() are zero (startNs === endNs). expect(tool.totalNs).toBe(0); diff --git a/src/perf/rollup.ts b/src/perf/rollup.ts index db0e7dbaa..74f407dae 100644 --- a/src/perf/rollup.ts +++ b/src/perf/rollup.ts @@ -65,7 +65,9 @@ function percentileNearestRank(sortedAsc: readonly number[], p: number): number // Nearest-rank: ceil(p * n), 1-indexed → 0-indexed clamp. const rank = Math.ceil(p * sortedAsc.length) - 1; const idx = Math.min(sortedAsc.length - 1, Math.max(0, rank)); - return sortedAsc[idx]!; + const value = sortedAsc[idx]; + if (value === undefined) return 0; + return value; } /** @@ -140,7 +142,8 @@ export function walkDescendants( // Copy so callers can mutate freely; walk iteratively to avoid deep recursion. const work: PerfSpan[] = stack.slice(); while (work.length > 0) { - const span = work.pop()!; + const span = work.pop(); + if (span === undefined) break; visit(span); const kids = byParent.get(span.id); if (kids !== undefined) { diff --git a/src/permission/approval-log.test.ts b/src/permission/approval-log.test.ts index 97a8a0b49..437bb5e7a 100644 --- a/src/permission/approval-log.test.ts +++ b/src/permission/approval-log.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -46,16 +47,16 @@ describe("createApprovalLog", () => { const [record] = readRecords(dir); expect(record).toBeDefined(); - expect(record!.tool).toBe("run_shell"); - expect(record!.mode).toBe("interactive"); - expect(record!.segments).toBe(3); - expect(record!.outcome).toBe("allow-with-scope"); - expect(record!.durationMs).toBe(150); - expect(record!.displayDelayMs).toBe(50); + expect(defined(record).tool).toBe("run_shell"); + expect(defined(record).mode).toBe("interactive"); + expect(defined(record).segments).toBe(3); + expect(defined(record).outcome).toBe("allow-with-scope"); + expect(defined(record).durationMs).toBe(150); + expect(defined(record).displayDelayMs).toBe(50); // No command text, path, or subject of any kind is ever recorded. - expect(Object.keys(record!)).not.toContain("subject"); - expect(Object.keys(record!)).not.toContain("command"); - expect(Object.keys(record!)).not.toContain("arguments"); + expect(Object.keys(defined(record))).not.toContain("subject"); + expect(Object.keys(defined(record))).not.toContain("command"); + expect(Object.keys(defined(record))).not.toContain("arguments"); }); test("settle is idempotent — a second call does not append twice", async () => { @@ -94,9 +95,9 @@ describe("approval-log wiring through the permission gate", () => { await new Promise((r) => setTimeout(r, 10)); const [record] = readRecords(dir); expect(record).toBeDefined(); - expect(record!.mode).toBe("auto"); - expect(record!.outcome).toBe("auto-deny"); - expect(record!.rule).toBe("file-mutation"); + expect(defined(record).mode).toBe("auto"); + expect(defined(record).outcome).toBe("auto-deny"); + expect(defined(record).rule).toBe("file-mutation"); const serialized = JSON.stringify(record); expect(serialized).not.toContain("hunter2"); expect(serialized).not.toContain("leaked-secret-file"); @@ -123,9 +124,9 @@ describe("approval-log wiring through the permission gate", () => { await new Promise((r) => setTimeout(r, 10)); const [record] = readRecords(dir); expect(record).toBeDefined(); - expect(record!.mode).toBe("interactive"); - expect(record!.outcome).toBe("allow-once"); - expect(typeof record!.displayDelayMs).toBe("number"); + expect(defined(record).mode).toBe("interactive"); + expect(defined(record).outcome).toBe("allow-once"); + expect(typeof defined(record).displayDelayMs).toBe("number"); const serialized = JSON.stringify(record); expect(serialized).not.toContain("super-secret-token"); expect(serialized).not.toContain("curl"); @@ -148,8 +149,8 @@ describe("approval-log wiring through the permission gate", () => { await new Promise((r) => setTimeout(r, 10)); const [record] = readRecords(dir); expect(record).toBeDefined(); - expect(record!.outcome).toBe("deny"); - expect(record!.rule).toBe("non-interactive"); + expect(defined(record).outcome).toBe("deny"); + expect(defined(record).rule).toBe("non-interactive"); }); // A sub-agent's `spawn_agent` dispatch `description` is model-authored free text @@ -185,7 +186,7 @@ describe("approval-log wiring through the permission gate", () => { await new Promise((r) => setTimeout(r, 10)); const [record] = readRecords(dir); expect(record).toBeDefined(); - expect(Object.keys(record!)).not.toContain("agentLabel"); + expect(Object.keys(defined(record))).not.toContain("agentLabel"); const serialized = JSON.stringify(record); expect(serialized).not.toContain(secret); expect(serialized).not.toContain("vault"); diff --git a/src/permission/approval-log.ts b/src/permission/approval-log.ts index 4c7c47d0c..0a2b89bbf 100644 --- a/src/permission/approval-log.ts +++ b/src/permission/approval-log.ts @@ -111,8 +111,8 @@ export interface ApprovalLog { export const NOOP_APPROVAL_LOG: ApprovalLog = { ask: () => ({ id: "", - markDisplayed: () => {}, - settle: () => {}, + markDisplayed: () => undefined, + settle: () => undefined, }), }; diff --git a/src/permission/auto-shell-policy.ts b/src/permission/auto-shell-policy.ts index 8942eeb94..2175b5a02 100644 --- a/src/permission/auto-shell-policy.ts +++ b/src/permission/auto-shell-policy.ts @@ -236,7 +236,8 @@ function segmentHasEnvAssignmentAsk(segment: string): boolean { if (envToken === undefined || envToken.replace(/^.*\//, "") !== "env") return false; i++; while (i < tokens.length) { - const t = tokens[i]!; + const t = tokens[i]; + if (t === undefined) break; if (t === "--") return false; if (t.startsWith("--split-string=")) { return payloadStartsWithAssignment(t.slice("--split-string=".length)); @@ -375,7 +376,8 @@ function worktreePathArgs( const paths: string[] = []; let force = false; for (let i = 0; i < args.length; i++) { - const arg = args[i]!; + const arg = args[i]; + if (arg === undefined) continue; if (arg === "--") { paths.push(...args.slice(i + 1)); break; @@ -425,7 +427,8 @@ export function safeWorktreeCommand( if (subcommand === "prune") { for (let i = 0; i < args.length; i++) { - const arg = args[i]!; + const arg = args[i]; + if (arg === undefined) continue; if (WORKTREE_PRUNE_FLAGS.has(arg)) continue; if (arg.startsWith("--expire=")) continue; if (arg === "--expire") { @@ -444,7 +447,9 @@ export function safeWorktreeCommand( // add/remove require a path; no path → ask rather than guess. if (paths.length === 0) return false; // First positional is the worktree path; later tokens on add are commit-ish. - return isContainedWorktreePath(paths[0]!, isRestricted, cwd, rootsProvider); + const pathArg = paths[0]; + if (pathArg === undefined) return false; + return isContainedWorktreePath(pathArg, isRestricted, cwd, rootsProvider); } // move / lock / unlock / repair / unknown — still ask until proven safe. diff --git a/src/permission/classify.ts b/src/permission/classify.ts index 027e421c6..5c3a0725c 100644 --- a/src/permission/classify.ts +++ b/src/permission/classify.ts @@ -143,7 +143,8 @@ function isBoundedDirectoryListing(program: string, args: readonly string[]): bo if (program === "tree") { if (args.some((arg) => TREE_FILE_IO_FLAG.test(arg))) return false; for (let i = 0; i < args.length; i++) { - const arg = args[i]!; + const arg = args[i]; + if (arg === undefined) continue; const depth = parseTreeDepth(arg, args[i + 1]); if (depth === undefined) continue; // `-L` / `--max-depth` consume the next token when separate. diff --git a/src/permission/command.ts b/src/permission/command.ts index e1dd6ee27..9c8439aa2 100644 --- a/src/permission/command.ts +++ b/src/permission/command.ts @@ -291,7 +291,8 @@ export function deriveCommandScopes(rawCommand: string): ApprovalScope[] { } const scopes: ApprovalScope[] = []; - const minPrefix = MULTIPLEXERS.has(tokens[0]!) ? 2 : 1; + const firstToken = tokens[0]; + const minPrefix = firstToken !== undefined && MULTIPLEXERS.has(firstToken) ? 2 : 1; const prefixLimit = Math.min(tokens.length - 1, minPrefix + MAX_PREFIX_SCOPES - 1); for (let n = minPrefix; n <= prefixLimit; n++) { const prefix = tokens.slice(0, n).join(" "); diff --git a/src/permission/gate.ts b/src/permission/gate.ts index 7d3b1a068..f3f33b404 100644 --- a/src/permission/gate.ts +++ b/src/permission/gate.ts @@ -865,10 +865,12 @@ export function createPermissionGate(options: PermissionGateOptions): Permission const removeSessionApproval = (target: Approval): void => { for (let i = approvals.length - 1; i >= 0; i--) { - if (sameApproval(approvals[i]!, target)) approvals.splice(i, 1); + const approval = approvals[i]; + if (approval !== undefined && sameApproval(approval, target)) approvals.splice(i, 1); } for (let i = sessionGrants.length - 1; i >= 0; i--) { - if (sameApproval(sessionGrants[i]!, target)) sessionGrants.splice(i, 1); + const grant = sessionGrants[i]; + if (grant !== undefined && sameApproval(grant, target)) sessionGrants.splice(i, 1); } }; diff --git a/src/permission/permission.test.ts b/src/permission/permission.test.ts index d044b4bca..6c149baea 100644 --- a/src/permission/permission.test.ts +++ b/src/permission/permission.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from "node:fs"; @@ -551,7 +552,7 @@ describe("buildRequests", () => { test("MCP tools are presented by a human label, not the raw identifier", () => { const reqs = buildRequests({ id: "c", name: "mcp__acme__list_projects", arguments: {} }); expect(reqs).toHaveLength(1); - const req = reqs[0]!; + const req = defined(reqs[0]); expect(req.action).not.toContain("mcp__"); expect(req.scopes[0]?.label).toBe("Always allow Acme: List Projects"); expect(req.scopes[0]?.hint).toBe("Acme: List Projects"); @@ -3300,7 +3301,7 @@ describe("listWorktreeRoots", () => { const { repo } = createRepoWithWorktree(); const roots = await listWorktreeRoots(repo); const plugin = pathEscapePlugin(repo, () => roots); - const handler = plugin.middleware!((call) => + const handler = defined(plugin.middleware)((call) => Promise.resolve({ callId: call.id, content: "ok" }), ); const result = await handler( @@ -3316,7 +3317,7 @@ describe("listWorktreeRoots", () => { const outside = mkdtempSync(join(tmpdir(), "intercode-unrelated-plugin-")); const relativeToOutside = relative(repo, join(outside, "payload.ts")); const plugin = pathEscapePlugin(repo, () => roots); - const handler = plugin.middleware!((call) => + const handler = defined(plugin.middleware)((call) => Promise.resolve({ callId: call.id, content: "ok" }), ); const result = await handler( diff --git a/src/plugins/agent-plugins.test.ts b/src/plugins/agent-plugins.test.ts index ac36e6ed2..7101681c5 100644 --- a/src/plugins/agent-plugins.test.ts +++ b/src/plugins/agent-plugins.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { resolveAgentPluginProfiles } from "./agent-plugins.js"; import type { PluginModule } from "./loader.js"; @@ -29,7 +30,7 @@ describe("resolveAgentPluginProfiles", () => { const { mod, config } = agentModule("p1", [validProfile]); const profiles = await resolveAgentPluginProfiles([mod], config); expect(profiles.length).toBe(1); - expect(profiles[0]!.id).toBe("scout"); + expect(defined(profiles[0]).id).toBe("scout"); }); test("skips profiles from disabled plugins", async () => { @@ -60,7 +61,7 @@ describe("resolveAgentPluginProfiles", () => { ]); const profiles = await resolveAgentPluginProfiles([mod], config); expect(profiles.length).toBe(1); - expect(profiles[0]!.id).toBe("scout"); + expect(defined(profiles[0]).id).toBe("scout"); }); test("collects from multiple plugins and flattens", async () => { @@ -83,14 +84,14 @@ describe("resolveAgentPluginProfiles", () => { test("stamps plugin: source for ordinary plugins", async () => { const { mod, config } = agentModule("p1", [validProfile]); const profiles = await resolveAgentPluginProfiles([mod], config); - expect(profiles[0]!.source).toBe("plugin:p1"); + expect(defined(profiles[0]).source).toBe("plugin:p1"); }); test("preserves mod.source when set (claude marketplace)", async () => { const { mod, config } = agentModule("p1", [validProfile]); mod.source = "claude"; const profiles = await resolveAgentPluginProfiles([mod], config); - expect(profiles[0]!.source).toBe("claude"); + expect(defined(profiles[0]).source).toBe("claude"); }); // Gating uses isPluginModuleEnabled (same as skills), not the bare diff --git a/src/plugins/agent-plugins.ts b/src/plugins/agent-plugins.ts index d63f27f1a..2f47c662d 100644 --- a/src/plugins/agent-plugins.ts +++ b/src/plugins/agent-plugins.ts @@ -25,7 +25,7 @@ function resolveAgentProfileWarningHandler( if (typeof opts === "function") return opts; if (opts.diagnostics !== undefined) return pluginWarningSink(opts.diagnostics); if (opts.onWarning !== undefined) return opts.onWarning; - return () => {}; + return () => undefined; } // Collect agent profiles from every enabled agent-kind plugin. Each profile is diff --git a/src/plugins/change-diff.test.ts b/src/plugins/change-diff.test.ts index 1fb1c3681..d46fe4b8b 100644 --- a/src/plugins/change-diff.test.ts +++ b/src/plugins/change-diff.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { formatChangeDiff, MAX_DIFF_CHARS } from "./change-diff.js"; describe("formatChangeDiff", () => { @@ -30,7 +31,7 @@ describe("formatChangeDiff", () => { expect(diff).toBeDefined(); // The cap must hold exactly — the truncation note is reserved WITHIN // maxChars, not appended after it. - expect(diff!.length).toBeLessThanOrEqual(MAX_DIFF_CHARS); + expect(defined(diff).length).toBeLessThanOrEqual(MAX_DIFF_CHARS); expect(diff).toContain("truncated"); }); @@ -44,7 +45,7 @@ describe("formatChangeDiff", () => { for (const maxChars of [50, 80, 120, 200]) { const diff = formatChangeDiff("a.txt", before, after, maxChars); expect(diff).toBeDefined(); - expect(diff!.length).toBeLessThanOrEqual(maxChars); + expect(defined(diff).length).toBeLessThanOrEqual(maxChars); } }); @@ -57,7 +58,7 @@ describe("formatChangeDiff", () => { expect(diff).toBeDefined(); expect(diff).toContain("large change"); expect(diff).toContain("exceeds"); - expect(diff!.length).toBeLessThanOrEqual(MAX_DIFF_CHARS); + expect(defined(diff).length).toBeLessThanOrEqual(MAX_DIFF_CHARS); }); test("deletion (after is empty) shows removed lines", () => { diff --git a/src/plugins/change-diff.ts b/src/plugins/change-diff.ts index 9f4a36d46..68230eb24 100644 --- a/src/plugins/change-diff.ts +++ b/src/plugins/change-diff.ts @@ -30,14 +30,29 @@ interface DiffOp { function lcsDiff(oldLines: string[], newLines: string[]): DiffOp[] { const n = oldLines.length; const m = newLines.length; - const dp: Uint32Array[] = new Array(n + 1); - for (let i = 0; i <= n; i++) dp[i] = new Uint32Array(m + 1); + const dp: Uint32Array[] = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1)); for (let i = n - 1; i >= 0; i--) { + const row = dp[i]; + const nextRow = dp[i + 1]; + if (row === undefined || nextRow === undefined) { + throw new Error("lcs dp row missing"); + } for (let j = m - 1; j >= 0; j--) { - dp[i]![j] = - oldLines[i] === newLines[j] - ? dp[i + 1]![j + 1]! + 1 - : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!); + const oldLine = oldLines[i]; + const newLine = newLines[j]; + const diag = nextRow[j + 1]; + const down = nextRow[j]; + const right = row[j + 1]; + if ( + oldLine === undefined || + newLine === undefined || + diag === undefined || + down === undefined || + right === undefined + ) { + throw new Error("lcs dp cell missing"); + } + row[j] = oldLine === newLine ? diag + 1 : Math.max(down, right); } } @@ -45,24 +60,35 @@ function lcsDiff(oldLines: string[], newLines: string[]): DiffOp[] { let i = 0; let j = 0; while (i < n && j < m) { - if (oldLines[i] === newLines[j]) { - ops.push({ kind: "same", text: oldLines[i]! }); + const oldLine = oldLines[i]; + const newLine = newLines[j]; + if (oldLine === undefined || newLine === undefined) break; + const nextRow = dp[i + 1]; + const row = dp[i]; + const down = nextRow?.[j]; + const right = row?.[j + 1]; + if (oldLine === newLine) { + ops.push({ kind: "same", text: oldLine }); i++; j++; - } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) { - ops.push({ kind: "del", text: oldLines[i]! }); + } else if (down !== undefined && right !== undefined && down >= right) { + ops.push({ kind: "del", text: oldLine }); i++; } else { - ops.push({ kind: "add", text: newLines[j]! }); + ops.push({ kind: "add", text: newLine }); j++; } } while (i < n) { - ops.push({ kind: "del", text: oldLines[i]! }); + const oldLine = oldLines[i]; + if (oldLine === undefined) break; + ops.push({ kind: "del", text: oldLine }); i++; } while (j < m) { - ops.push({ kind: "add", text: newLines[j]! }); + const newLine = newLines[j]; + if (newLine === undefined) break; + ops.push({ kind: "add", text: newLine }); j++; } return ops; @@ -90,7 +116,8 @@ function toHunks(ops: DiffOp[]): Hunk[] { }; for (let idx = 0; idx < ops.length; idx++) { - const op = ops[idx]!; + const op = ops[idx]; + if (op === undefined) continue; if (op.kind === "same") { sameRun++; if (cur !== undefined) { diff --git a/src/plugins/claude-plugins.test.ts b/src/plugins/claude-plugins.test.ts index 57eaed0a1..1211c72ec 100644 --- a/src/plugins/claude-plugins.test.ts +++ b/src/plugins/claude-plugins.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { symlinkSync } from "node:fs"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; @@ -53,10 +54,10 @@ describe("discoverClaudeInstalledPlugins", () => { const modules = await discoverClaudeInstalledPlugins("/repo", { home }); expect(modules.length).toBe(1); - expect(modules[0]!.source).toBe("claude"); - expect(modules[0]!.origin).toBe("user"); - expect(modules[0]!.manifest?.id).toBe("demo-agent"); - expect(modules[0]!.agentPlugin?.agents.length).toBeGreaterThan(0); + expect(defined(modules[0]).source).toBe("claude"); + expect(defined(modules[0]).origin).toBe("user"); + expect(defined(modules[0]).manifest?.id).toBe("demo-agent"); + expect(defined(modules[0]).agentPlugin?.agents.length).toBeGreaterThan(0); // Enable-gate still applies: disabled config yields no profiles. expect(await resolveAgentPluginProfiles(modules, {})).toEqual([]); @@ -132,8 +133,8 @@ describe("discoverClaudeInstalledPlugins", () => { const modules = await discoverClaudeInstalledPlugins("/repo", { home }); expect(modules.length).toBe(1); - expect(modules[0]!.manifest?.id).toBe("cmo"); - expect(modules[0]!.source).toBe("claude"); + expect(defined(modules[0]).manifest?.id).toBe("cmo"); + expect(defined(modules[0]).source).toBe("claude"); }); test("rewrites version-dir basename ids using the registry key", async () => { @@ -156,7 +157,7 @@ describe("discoverClaudeInstalledPlugins", () => { const modules = await discoverClaudeInstalledPlugins("/repo", { home }); expect(modules.length).toBe(1); - expect(modules[0]!.manifest?.id).toBe("orphan"); + expect(defined(modules[0]).manifest?.id).toBe("orphan"); }); test("rejects installPath outside ~/.claude/plugins and relative paths", async () => { @@ -263,8 +264,8 @@ describe("discoverClaudeInstalledPlugins", () => { const modules = await discoverClaudeInstalledPlugins("/repo", { home }); expect(modules.map((m) => m.manifest?.id)).toEqual(["scout-agent"]); - expect(modules[0]!.source).toBe("claude"); - expect(modules[0]!.pluginPath).toBe(agentDir); + expect(defined(modules[0]).source).toBe("claude"); + expect(defined(modules[0]).pluginPath).toBe(agentDir); }); test("rejects absolute marketplace sources and escapes outside ~/.claude/plugins", async () => { diff --git a/src/plugins/data-only-agent.ts b/src/plugins/data-only-agent.ts index 09dbbee83..a4deeebca 100644 --- a/src/plugins/data-only-agent.ts +++ b/src/plugins/data-only-agent.ts @@ -320,7 +320,8 @@ function parseSkillReferencesFromBody(body: string): string[] { const re = /\bload\s+the\s+`([a-z0-9_-]+)`\s+skill\b/gi; let match: RegExpExecArray | null; while ((match = re.exec(body)) !== null) { - out.push(match[1]!); + const name = match[1]; + if (name !== undefined) out.push(name); } return out; } diff --git a/src/plugins/data-only-commands.ts b/src/plugins/data-only-commands.ts index 14b612cd9..9aa877ed4 100644 --- a/src/plugins/data-only-commands.ts +++ b/src/plugins/data-only-commands.ts @@ -111,7 +111,7 @@ export async function loadDataOnlyCommands( pluginDir: string, opts: { onWarning?: (msg: string) => void } = {}, ): Promise<{ commandPlugin: CommandPlugin } | null> { - const warn = opts.onWarning ?? (() => {}); + const warn = opts.onWarning ?? (() => undefined); // Accept both `commands/` (Claude Code) and `command/` (OpenCode) roots. let root: string | null = null; diff --git a/src/plugins/delete-file-plugin.test.ts b/src/plugins/delete-file-plugin.test.ts index be8acbce2..3fa2f4e00 100644 --- a/src/plugins/delete-file-plugin.test.ts +++ b/src/plugins/delete-file-plugin.test.ts @@ -30,7 +30,7 @@ describe("deleteFilePlugin", () => { }); afterEach(async () => { - await chmod(cwd, 0o700).catch(() => {}); + await chmod(cwd, 0o700).catch(() => undefined); await rm(cwd, { recursive: true, force: true }); }); diff --git a/src/plugins/diagnostics.test.ts b/src/plugins/diagnostics.test.ts index 86f02fd41..1febccc5c 100644 --- a/src/plugins/diagnostics.test.ts +++ b/src/plugins/diagnostics.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -194,8 +195,8 @@ describe("plugin load diagnostics wiring", () => { const summary = formatPluginWarningsSummary(diag.warnings); expect(summary).toBeDefined(); - expect(summary!.startsWith("plugins:")).toBe(true); + expect(defined(summary).startsWith("plugins:")).toBe(true); // One summary line, not N raw plugins: lines from default sink. - expect(summary!.split("\n").length).toBe(1); + expect(defined(summary).split("\n").length).toBe(1); }); }); diff --git a/src/plugins/edit-file-diagnostics-plugin.test.ts b/src/plugins/edit-file-diagnostics-plugin.test.ts index 9b19f731f..6bdbb2e19 100644 --- a/src/plugins/edit-file-diagnostics-plugin.test.ts +++ b/src/plugins/edit-file-diagnostics-plugin.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -51,9 +52,9 @@ describe("normalizeLine / near-miss helpers", () => { const old = ["const bareKey = 1;", " const entry = 2;"].join("\n"); const miss = findWhitespaceNearMiss(file, old); expect(miss).not.toBeNull(); - expect(miss!.text).toBe(" const bareKey = 1;\n const entry = 2;"); - expect(miss!.startLine).toBe(2); - expect(miss!.endLine).toBe(3); + expect(defined(miss).text).toBe(" const bareKey = 1;\n const entry = 2;"); + expect(defined(miss).startLine).toBe(2); + expect(defined(miss).endLine).toBe(3); }); test("findWhitespaceNearMiss ignores trailing and leading newlines on the needle", () => { @@ -63,7 +64,7 @@ describe("normalizeLine / near-miss helpers", () => { for (const old of [trailing, leading]) { const miss = findWhitespaceNearMiss(file, old); expect(miss).not.toBeNull(); - expect(miss!.text).toBe(" const bareKey = 1;\n const entry = 2;"); + expect(defined(miss).text).toBe(" const bareKey = 1;\n const entry = 2;"); } }); @@ -78,8 +79,8 @@ describe("normalizeLine / near-miss helpers", () => { const miss = findWhitespaceNearMiss(file, old); expect(miss).not.toBeNull(); // Original span uses the split-on-\n form; trailing \r may remain on the line body. - expect(normalizeLine(miss!.text)).toBe("const x = 1;"); - expect(miss!.startLine).toBe(2); + expect(normalizeLine(defined(miss).text)).toBe("const x = 1;"); + expect(defined(miss).startLine).toBe(2); }); test("stripLineNumberPrefixes detects read_file decoration", () => { @@ -138,9 +139,9 @@ describe("editFileDiagnosticsPlugin", () => { extra: Middleware[] = [], ) { const mws: Middleware[] = [ - pathEscapePlugin(cwd).middleware!, - verifyPlugin().middleware!, - editFileDiagnosticsPlugin().middleware!, + defined(pathEscapePlugin(cwd).middleware), + defined(verifyPlugin().middleware), + defined(editFileDiagnosticsPlugin().middleware), ...extra, ]; return composeMiddleware(mws, next); diff --git a/src/plugins/edit-file-diagnostics-plugin.ts b/src/plugins/edit-file-diagnostics-plugin.ts index a5d3ebb02..f75c91786 100644 --- a/src/plugins/edit-file-diagnostics-plugin.ts +++ b/src/plugins/edit-file-diagnostics-plugin.ts @@ -191,7 +191,8 @@ export function findWhitespaceNearMiss(fileText: string, oldString: string): Nea return null; } - const hit = hits[0]!; + const hit = hits[0]; + if (hit === undefined) return null; // Reconstruct original span with "\n" join — matches how edit_file treats content. const text = fileLines.slice(hit.start, hit.end + 1).join("\n"); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index fa50c6cef..013c67fc8 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { dedupePluginModules, type PluginModule } from "./loader.js"; import { isPluginModuleEnabled } from "./register.js"; @@ -32,7 +33,7 @@ describe("dedupePluginModules", () => { const repo = repoDefaultEnabled("scout"); const user = userInstall("scout"); const [result] = dedupePluginModules([repo, user]); - expect(result!.shadowedRepoDefaultEnabled).toBe(true); + expect(defined(result).shadowedRepoDefaultEnabled).toBe(true); }); test("does not stamp shadowedRepoDefaultEnabled when the repo module wasn't defaultEnabled", () => { @@ -42,7 +43,7 @@ describe("dedupePluginModules", () => { }; const user = userInstall("scout"); const [result] = dedupePluginModules([repo, user]); - expect(result!.shadowedRepoDefaultEnabled).toBeUndefined(); + expect(defined(result).shadowedRepoDefaultEnabled).toBeUndefined(); }); test("does not stamp unrelated ids", () => { @@ -50,7 +51,7 @@ describe("dedupePluginModules", () => { const other = userInstall("other"); const result = dedupePluginModules([repo, other]); expect( - result.find((m) => m.manifest?.id === "other")!.shadowedRepoDefaultEnabled, + defined(result.find((m) => m.manifest?.id === "other")).shadowedRepoDefaultEnabled, ).toBeUndefined(); }); @@ -63,7 +64,7 @@ describe("dedupePluginModules", () => { }; const [result] = dedupePluginModules([repo, user, path]); expect(result).toMatchObject({ origin: "path" }); - expect(result!.shadowedRepoDefaultEnabled).toBe(true); + expect(defined(result).shadowedRepoDefaultEnabled).toBe(true); }); }); @@ -72,24 +73,24 @@ describe("isPluginModuleEnabled with dedupe shadowing", () => { const repo = repoDefaultEnabled("scout"); const user = userInstall("scout"); const [survivor] = dedupePluginModules([repo, user]); - expect(isPluginModuleEnabled(survivor!, {})).toBe(true); + expect(isPluginModuleEnabled(defined(survivor), {})).toBe(true); }); test("an explicit disable in settings still wins over the preserved default-on", () => { const repo = repoDefaultEnabled("scout"); const user = userInstall("scout"); const [survivor] = dedupePluginModules([repo, user]); - expect(isPluginModuleEnabled(survivor!, { scout: { enabled: false } })).toBe(false); + expect(isPluginModuleEnabled(defined(survivor), { scout: { enabled: false } })).toBe(false); }); test("disablePluginSettings then isPluginModuleEnabled is false for shadowedRepoDefaultEnabled", () => { const repo = repoDefaultEnabled("scout"); const user = userInstall("scout"); const [survivor] = dedupePluginModules([repo, user]); - expect(survivor!.shadowedRepoDefaultEnabled).toBe(true); + expect(defined(survivor).shadowedRepoDefaultEnabled).toBe(true); const plugins = disablePluginSettings({}, "scout"); expect(plugins.scout?.enabled).toBe(false); - expect(isPluginModuleEnabled(survivor!, plugins)).toBe(false); + expect(isPluginModuleEnabled(defined(survivor), plugins)).toBe(false); }); test("without dedupe shadowing, a plain user-origin module needs an explicit enable", () => { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index f44f5b768..d649b7155 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -303,7 +303,7 @@ export interface ExpandPluginPathOptions { * into a compile error until it picks a handler on purpose: * `expandSkipDiagnosticsHandler(diagnostics)` for a batching caller, * an explicit stderr writer for a headless caller where that is correct - * and visible (see `src/exec/runner.ts`), or `() => {}` to state on the + * and visible (see `src/exec/runner.ts`), or `() => undefined` to state on the * record that a caller is deliberately ignoring skips. */ onSkip: (skip: ExpandPluginPathSkip) => void; @@ -433,7 +433,8 @@ export async function expandPluginPath( const existing = await Promise.all(candidates.map((c) => pathExists(c.resolved))); const surviving: string[] = []; for (let i = 0; i < candidates.length; i++) { - const c = candidates[i]!; + const c = candidates[i]; + if (c === undefined) continue; if (existing[i]) { surviving.push(c.resolved); } else { @@ -606,7 +607,8 @@ export function dedupePluginModules(modules: PluginModule[]): PluginModule[] { } const existing = indexById.get(id); if (existing !== undefined) { - const prev = result[existing]!; + const prev = result[existing]; + if (prev === undefined) continue; const wasRepoDefaultEnabled = prev.shadowedRepoDefaultEnabled === true || (prev.origin === "repo" && prev.manifest?.defaultEnabled === true); diff --git a/src/plugins/read-file-guard-plugin.test.ts b/src/plugins/read-file-guard-plugin.test.ts index 0bfeb0011..ec14a2920 100644 --- a/src/plugins/read-file-guard-plugin.test.ts +++ b/src/plugins/read-file-guard-plugin.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -180,7 +181,7 @@ describe("readFileGuardPlugin", () => { blobReader?: ReturnType, ): Promise { const plugin = readFileGuardPlugin(dir, blobReader !== undefined ? { blobReader } : {}); - return plugin.middleware!(fallback)(call, neverAbort()); + return defined(plugin.middleware)(fallback)(call, neverAbort()); } test("intercepts read_file for real paths", async () => { @@ -282,7 +283,7 @@ describe("readFileGuardPlugin", () => { test("a truncated read never asks the model to re-read the same path (CL-6961)", async () => { await fixture("many-lines.txt", Array.from({ length: 10 }, (_, i) => `line-${i}`).join("\n")); const plugin = readFileGuardPlugin(dir, {}); - const middleware = plugin.middleware!(fallback); + const middleware = defined(plugin.middleware)(fallback); const result = await middleware( { id: "c1", name: "read_file", arguments: { path: "many-lines.txt", limit: 4 } }, neverAbort(), @@ -297,7 +298,7 @@ describe("readFileGuardPlugin", () => { const lines = Array.from({ length: 9_000 }, (_, i) => `line-${i} payload`); await fixture("huge.txt", lines.join("\n")); const plugin = readFileGuardPlugin(dir, {}); - const middleware = plugin.middleware!(fallback); + const middleware = defined(plugin.middleware)(fallback); const pathsRead: string[] = ["huge.txt"]; let result = await middleware( @@ -337,7 +338,7 @@ describe("readFileGuardPlugin", () => { Array.from({ length: 10 }, (_, i) => `line-${i}`).join("\n"), ); const plugin = readFileGuardPlugin(dir, {}); - const middleware = plugin.middleware!(fallback); + const middleware = defined(plugin.middleware)(fallback); const first = await middleware( { id: "s1", name: "read_file", arguments: { path: "stale.txt", limit: 4 } }, neverAbort(), @@ -390,7 +391,7 @@ describe("readFileGuardPlugin", () => { }, }; const plugin = readFileGuardPlugin(dir, { blobReader }); - const middleware = plugin.middleware!(fallback); + const middleware = defined(plugin.middleware)(fallback); const first = await middleware( { id: "b1", name: "read_file", arguments: { path: "tool-output:///spill-1", limit: 5 } }, diff --git a/src/plugins/register.ts b/src/plugins/register.ts index ffb11aabb..09da2716d 100644 --- a/src/plugins/register.ts +++ b/src/plugins/register.ts @@ -98,8 +98,11 @@ export function registerWorkflowPlugins( const registered: string[] = []; for (const mod of modules) { if (!isEnabledWorkflowPlugin(mod, config)) continue; - registerWorkflowPlugin(mod.workflowPlugin!); - registered.push(mod.manifest!.id); + const workflowPlugin = mod.workflowPlugin; + const id = mod.manifest?.id; + if (workflowPlugin === undefined || id === undefined) continue; + registerWorkflowPlugin(workflowPlugin); + registered.push(id); } return registered; } diff --git a/src/plugins/result-truncation-plugin.test.ts b/src/plugins/result-truncation-plugin.test.ts index 3e1489049..f4de43883 100644 --- a/src/plugins/result-truncation-plugin.test.ts +++ b/src/plugins/result-truncation-plugin.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { createSizeCapTransform } from "@intx/inference"; import { createBlobReader, type StrategyContext, type ToolResult } from "@intx/types/runtime"; @@ -110,7 +111,7 @@ describe("truncateToolResultContent", () => { const entry = store.blobs.get(key); expect(entry).toBeDefined(); expect(entry?.contentType).toBe("application/json"); - expect(new TextDecoder().decode(entry!.bytes)).toBe(pretty); + expect(new TextDecoder().decode(defined(entry).bytes)).toBe(pretty); const uri = `tool-output:///${key}`; const abs = toolOutputAbsolutePath("/tmp/session/context", key, "application/json"); @@ -132,7 +133,7 @@ describe("truncateToolResultContent", () => { }); const spilled = new TextDecoder().decode( - store.blobs.get(spillBlobKey("call-json-secret"))!.bytes, + defined(store.blobs.get(spillBlobKey("call-json-secret"))).bytes, ); expect(truncated).toContain(CREDENTIAL_REDACTION); expect(truncated).not.toContain("sk-live-"); @@ -157,7 +158,7 @@ describe("truncateToolResultContent", () => { const key = spillBlobKey("call-ndjson"); const entry = store.blobs.get(key); expect(entry?.contentType).toBe("application/x-ndjson"); - expect(new TextDecoder().decode(entry!.bytes)).toBe(ndjson); + expect(new TextDecoder().decode(defined(entry).bytes)).toBe(ndjson); expect(truncated).toContain("application/x-ndjson"); }); @@ -232,7 +233,7 @@ describe("truncateToolResultContent", () => { await blobReader.read(`tool-output:///${spillBlobKey("call-1")}`), ); expect(recovered).toBe(original); - expect(new TextDecoder().decode(store.blobs.get("call-1")!.bytes)).toBe("LOSSY"); + expect(new TextDecoder().decode(defined(store.blobs.get("call-1")).bytes)).toBe("LOSSY"); }); }); }); @@ -303,7 +304,7 @@ describe("resultTruncationPlugin", () => { const key = spillBlobKey("call-rec"); const entry = store.blobs.get(key); expect(entry?.contentType).toBe("application/json"); - expect(new TextDecoder().decode(entry!.bytes)).toBe(JSON.stringify(record, null, 2)); + expect(new TextDecoder().decode(defined(entry).bytes)).toBe(JSON.stringify(record, null, 2)); }); test("under-gate Record content is left unchanged", async () => { @@ -352,7 +353,7 @@ describe("scrub-before-spill", () => { expect(String(result.content)).toContain(CREDENTIAL_REDACTION); expect(String(result.content)).not.toContain("sk-live-"); - const spilled = new TextDecoder().decode(store.blobs.get(spillBlobKey("call-scrub"))!.bytes); + const spilled = new TextDecoder().decode(defined(store.blobs.get(spillBlobKey("call-scrub"))).bytes); expect(spilled).toContain(CREDENTIAL_REDACTION); expect(spilled).not.toContain("sk-live-"); }); diff --git a/src/plugins/shell-guard-plugin.test.ts b/src/plugins/shell-guard-plugin.test.ts index c41b2c8e2..d77a91371 100644 --- a/src/plugins/shell-guard-plugin.test.ts +++ b/src/plugins/shell-guard-plugin.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { expect, test, describe } from "bun:test"; import { mkdtemp, mkdir } from "node:fs/promises"; import { join } from "node:path"; @@ -205,9 +206,9 @@ describe("background run_shell (shellGuardPlugin)", () => { }); function handlerWith(registry?: ReturnType) { - return shellGuardPlugin(process.cwd(), undefined, undefined, { + return defined(shellGuardPlugin(process.cwd(), undefined, undefined, { ...(registry !== undefined ? { getBackgroundShellRegistry: () => registry } : {}), - }).middleware!(fallback); + }).middleware)(fallback); } function runWith(registry: ReturnType, call: ToolCall) { @@ -357,7 +358,7 @@ describe("shellGuardPlugin", () => { }); function run(call: ToolCall): Promise { - const handler = shellGuardPlugin(process.cwd()).middleware!(fallback); + const handler = defined(shellGuardPlugin(process.cwd()).middleware)(fallback); return handler(call, neverAbort()); } @@ -372,9 +373,9 @@ describe("shellGuardPlugin", () => { }); test("plugin-level env is applied to run_shell's spawn environment", async () => { - const handler = shellGuardPlugin(process.cwd(), undefined, { + const handler = defined(shellGuardPlugin(process.cwd(), undefined, { CORBITS_TEST_ENV_VAR: "plugin-env", - }).middleware!(fallback); + }).middleware)(fallback); const result = await handler( { id: "c-env", name: "run_shell", arguments: { command: "echo $CORBITS_TEST_ENV_VAR" } }, neverAbort(), @@ -394,7 +395,7 @@ describe("shellGuardPlugin", () => { }); test("clamps a per-command timeout override to the configured max", async () => { - const handler = shellGuardPlugin(process.cwd(), { maxMs: 100 }).middleware!(fallback); + const handler = defined(shellGuardPlugin(process.cwd(), { maxMs: 100 }).middleware)(fallback); const result = await handler( { id: "c2b", name: "run_shell", arguments: { command: "sleep 60", timeout: 900_000 } }, neverAbort(), @@ -403,7 +404,7 @@ describe("shellGuardPlugin", () => { }); test("applies a configured default timeout when none is passed", async () => { - const handler = shellGuardPlugin(process.cwd(), { defaultMs: 90 }).middleware!(fallback); + const handler = defined(shellGuardPlugin(process.cwd(), { defaultMs: 90 }).middleware)(fallback); const result = await handler( { id: "c2c", name: "run_shell", arguments: { command: "sleep 60" } }, neverAbort(), @@ -412,7 +413,7 @@ describe("shellGuardPlugin", () => { }); test("omitted timeout with no settings default does not time out", async () => { - const handler = shellGuardPlugin(process.cwd()).middleware!(fallback); + const handler = defined(shellGuardPlugin(process.cwd()).middleware)(fallback); const result = await handler( { id: "c2d", name: "run_shell", arguments: { command: "sleep 0.2; echo ok" } }, neverAbort(), @@ -422,7 +423,7 @@ describe("shellGuardPlugin", () => { }); test("maxMs alone does not invent a timeout when the model omits timeout", async () => { - const handler = shellGuardPlugin(process.cwd(), { maxMs: 50 }).middleware!(fallback); + const handler = defined(shellGuardPlugin(process.cwd(), { maxMs: 50 }).middleware)(fallback); const result = await handler( { id: "c2e", name: "run_shell", arguments: { command: "sleep 0.2; echo ok" } }, neverAbort(), @@ -447,7 +448,7 @@ describe("shellGuardPlugin", () => { " 1\tpartial\n\nread_file [timed out before completing] for big.log — use a smaller offset/limit. This is not an empty file.", isError: true, }); - const handler = shellGuardPlugin(process.cwd()).middleware!(stockTimeout); + const handler = defined(shellGuardPlugin(process.cwd()).middleware)(stockTimeout); const result = await handler( { id: "c3b", name: "read_file", arguments: { path: "big.log" } }, neverAbort(), @@ -484,7 +485,7 @@ describe("shellGuardPlugin", () => { const plugin = shellGuardPlugin(process.cwd()); // Inject a fast abort parent so the search budget settles quickly. const controller = new AbortController(); - const handler = plugin.middleware!(slow); + const handler = defined(plugin.middleware)(slow); const promise = handler( { id: "c4", name: "grep", arguments: { pattern: "x" } }, controller.signal, @@ -498,7 +499,7 @@ describe("shellGuardPlugin", () => { test("rejects retaining cwd outside the session workspace", async () => { const root = await mkdtemp(join(tmpdir(), "ic-escape-cwd-")); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); const escaped = await handler( { id: "e1", name: "run_shell", arguments: { command: "cd .. && pwd" } }, neverAbort(), @@ -515,9 +516,9 @@ describe("shellGuardPlugin", () => { test("allowOutsideCwd getter allows retaining cwd outside the session workspace", async () => { const root = await mkdtemp(join(tmpdir(), "ic-escape-cwd-yolo-")); let allow = false; - const handler = shellGuardPlugin(root, undefined, undefined, { + const handler = defined(shellGuardPlugin(root, undefined, undefined, { allowOutsideCwd: () => allow, - }).middleware!(fallback); + }).middleware)(fallback); const blocked = await handler( { id: "e1", name: "run_shell", arguments: { command: "cd .. && pwd" } }, neverAbort(), @@ -538,7 +539,7 @@ describe("shellGuardPlugin", () => { const root = await mkdtemp(join(tmpdir(), "ic-cd-fail-")); const nested = join(root, "nested"); await mkdir(nested); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); const fail = await handler( { id: "cf1", name: "run_shell", arguments: { command: "cd nested && false" } }, neverAbort(), @@ -555,7 +556,7 @@ describe("shellGuardPlugin", () => { const root = await mkdtemp(join(tmpdir(), "ic-retain-cwd-")); const sub = join(root, "nested"); await mkdir(sub); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); const cdResult = await handler( { id: "cd1", name: "run_shell", arguments: { command: "cd nested" } }, neverAbort(), @@ -572,7 +573,7 @@ describe("shellGuardPlugin", () => { const root = await mkdtemp(join(tmpdir(), "ic-override-cwd-")); const sub = join(root, "other"); await mkdir(sub); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); await handler( { id: "o1", name: "run_shell", arguments: { command: "cd other" } }, neverAbort(), @@ -599,8 +600,8 @@ describe("shellGuardPlugin", () => { const b = join(root, "b"); await mkdir(a); await mkdir(b); - const handlerA = shellGuardPlugin(root).middleware!(fallback); - const handlerB = shellGuardPlugin(root).middleware!(fallback); + const handlerA = defined(shellGuardPlugin(root).middleware)(fallback); + const handlerB = defined(shellGuardPlugin(root).middleware)(fallback); await handlerA({ id: "ia", name: "run_shell", arguments: { command: "cd a" } }, neverAbort()); await handlerB({ id: "ib", name: "run_shell", arguments: { command: "cd b" } }, neverAbort()); const pwdA = await handlerA( @@ -619,7 +620,7 @@ describe("shellGuardPlugin", () => { const root = await mkdtemp(join(tmpdir(), "ic-serial-cwd-")); const nested = join(root, "nested"); await mkdir(nested); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); // Barrier: hold a non-cd command open while a cd is enqueued behind it. // Without serialization the waiter would finish after cd and clobber cwd. const release = join(root, "release"); @@ -651,7 +652,7 @@ describe("shellGuardPlugin", () => { const b = join(root, "b"); await mkdir(a); await mkdir(b); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); await Promise.all([ handler( { id: "f1", name: "run_shell", arguments: { command: `cd ${JSON.stringify(a)}` } }, @@ -672,7 +673,7 @@ describe("shellGuardPlugin", () => { test("surfaces a clear error when retained cwd is missing", async () => { const root = await mkdtemp(join(tmpdir(), "ic-missing-cwd-")); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); const gone = join(root, "removed"); await mkdir(gone); await handler( @@ -704,7 +705,7 @@ describe("shellGuardPlugin", () => { const prev = process.cwd(); try { await chdir(otherRoot); - const handler = shellGuardPlugin(root).middleware!(fallback); + const handler = defined(shellGuardPlugin(root).middleware)(fallback); const result = await handler( { id: "pc1", @@ -721,7 +722,7 @@ describe("shellGuardPlugin", () => { }); test("treats timeout 0 as the configured default", async () => { - const handler = shellGuardPlugin(process.cwd(), { defaultMs: 90, maxMs: 100 }).middleware!( + const handler = defined(shellGuardPlugin(process.cwd(), { defaultMs: 90, maxMs: 100 }).middleware)( fallback, ); const result = await handler( @@ -735,10 +736,10 @@ describe("shellGuardPlugin", () => { // Reproduces the non-abortable fallback grep: next() never settles and never // observes the abort. The guard must stop waiting once the budget fires // instead of awaiting the walk forever. - const hangs = (): Promise => new Promise(() => {}); + const hangs = (): Promise => new Promise(() => undefined); const plugin = shellGuardPlugin(process.cwd()); const controller = new AbortController(); - const handler = plugin.middleware!(hangs); + const handler = defined(plugin.middleware)(hangs); const promise = handler( { id: "c5", name: "grep", arguments: { pattern: "x" } }, controller.signal, @@ -752,7 +753,7 @@ describe("shellGuardPlugin", () => { test("dispose without abort kills tagged grandchildren and is idempotent", async () => { if (process.platform === "win32") return; const plugin = shellGuardPlugin(process.cwd()); - const handler = plugin.middleware!(fallback); + const handler = defined(plugin.middleware)(fallback); const token = `ic_guard_dispose_${randomUUID()}`; const cmd = `bash -c 'IC_GUARD_TAG=${token} sleep 600 & IC_GUARD_TAG=${token} exec sleep 600'`; const running = handler( @@ -770,12 +771,12 @@ describe("shellGuardPlugin", () => { "", ); expect(plugin.dispose).toBeDefined(); - await plugin.dispose!(); + await defined(plugin.dispose)(); await new Promise((r) => setTimeout(r, 300)); const after = spawnSync("pgrep", ["-f", token], { encoding: "utf8" }); expect(after.stdout?.trim() ?? "").toBe(""); expect(after.status).not.toBe(0); - await plugin.dispose!(); + await defined(plugin.dispose)(); await running; } finally { spawnSync("pkill", ["-9", "-f", token]); @@ -785,7 +786,7 @@ describe("shellGuardPlugin", () => { test("dispose refuses a queued run_shell so it cannot stay running after reap", async () => { if (process.platform === "win32") return; const plugin = shellGuardPlugin(process.cwd()); - const handler = plugin.middleware!(fallback); + const handler = defined(plugin.middleware)(fallback); const token1 = `ic_guard_queued1_${randomUUID()}`; const token2 = `ic_guard_queued2_${randomUUID()}`; const first = handler( @@ -818,7 +819,7 @@ describe("shellGuardPlugin", () => { let disposeError: unknown; try { - await plugin.dispose!(); + await defined(plugin.dispose)(); } catch (err) { disposeError = err; } @@ -852,7 +853,7 @@ describe("shellGuardPlugin", () => { test("overlapping dispose joins the in-flight reap", async () => { if (process.platform === "win32") return; const plugin = shellGuardPlugin(process.cwd()); - const handler = plugin.middleware!(fallback); + const handler = defined(plugin.middleware)(fallback); const token = `ic_guard_join_${randomUUID()}`; const running = handler( { @@ -870,8 +871,8 @@ describe("shellGuardPlugin", () => { await new Promise((r) => setTimeout(r, 50)); } expect(plugin.dispose).toBeDefined(); - const first = plugin.dispose!(); - const second = plugin.dispose!(); + const first = defined(plugin.dispose)(); + const second = defined(plugin.dispose)(); expect(second).toBe(first); await Promise.all([first, second]); await running; diff --git a/src/plugins/shell-guard-plugin.ts b/src/plugins/shell-guard-plugin.ts index 95a7da70b..9b27ffb57 100644 --- a/src/plugins/shell-guard-plugin.ts +++ b/src/plugins/shell-guard-plugin.ts @@ -174,7 +174,8 @@ export class BoundedShellOutput { this.tailChunks.push(buf); this.tailBytes += buf.length; while (this.tailBytes > this.tailMax && this.tailChunks.length > 0) { - const first = this.tailChunks[0]!; + const first = this.tailChunks[0]; + if (first === undefined) break; if (this.tailBytes - first.length >= this.tailMax) { this.tailBytes -= first.length; this.tailChunks.shift(); diff --git a/src/plugins/skill-commands.ts b/src/plugins/skill-commands.ts index 86e981958..6e2248e24 100644 --- a/src/plugins/skill-commands.ts +++ b/src/plugins/skill-commands.ts @@ -36,7 +36,7 @@ export async function loadSkillCommands( pluginDir: string, opts: { onWarning?: (msg: string) => void } = {}, ): Promise { - const warn = opts.onWarning ?? (() => {}); + const warn = opts.onWarning ?? (() => undefined); const skillsDir = join(pluginDir, "skills"); let entries: import("node:fs").Dirent[]; try { diff --git a/src/plugins/tool-plugins.test.ts b/src/plugins/tool-plugins.test.ts index 28943530b..7679be4f6 100644 --- a/src/plugins/tool-plugins.test.ts +++ b/src/plugins/tool-plugins.test.ts @@ -1,4 +1,5 @@ import { describe, test, expect } from "bun:test"; +import { defined } from "../../tests/helpers/defined.js"; import { collectToolPlugins, isToolPluginActive, @@ -61,7 +62,7 @@ describe("resolveToolPlugins", () => { }, }); expect(plugins.length).toBe(1); - expect(plugins[0]!.tools![0]!.definition.name).toBe("t1_tool"); + expect(defined(defined(defined(plugins[0]).tools)[0]).definition.name).toBe("t1_tool"); }); test("a throwing factory is skipped, not fatal", async () => { diff --git a/src/plugins/tool-result-secret-scrub.test.ts b/src/plugins/tool-result-secret-scrub.test.ts index 0a84cab92..e46436fc4 100644 --- a/src/plugins/tool-result-secret-scrub.test.ts +++ b/src/plugins/tool-result-secret-scrub.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { CREDENTIAL_REDACTION, scrubSecretShapedContent } from "./tool-result-secret-scrub.js"; import { toolResultSecretScrubPlugin } from "./tool-result-secret-scrub-plugin.js"; @@ -44,7 +45,7 @@ describe("toolResultSecretScrubPlugin", () => { test("scrubs grep tool results", async () => { const plugin = toolResultSecretScrubPlugin(); - const handler = plugin.middleware!(next("secrets/.env:1:TOKEN=supersecretvalue")); + const handler = defined(plugin.middleware)(next("secrets/.env:1:TOKEN=supersecretvalue")); const result = await handler( { id: "c1", name: "grep", arguments: { pattern: "TOKEN" } }, new AbortController().signal, @@ -82,7 +83,7 @@ describe("toolResultSecretScrubPlugin", () => { const body = "Matching agent profiles:\n\n### leaky\n\nSystem prompt / body:\n" + "Use API_KEY=sk-live-abc123xyz789012345678 when calling the provider."; - const handler = plugin.middleware!(next(body)); + const handler = defined(plugin.middleware)(next(body)); const result = await handler( { id: "c2", name: "search_agents", arguments: { query: "leaky" } }, new AbortController().signal, diff --git a/src/pricing-fetcher.test.ts b/src/pricing-fetcher.test.ts index 7cfa893ca..1fc80dae0 100644 --- a/src/pricing-fetcher.test.ts +++ b/src/pricing-fetcher.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { homedir } from "node:os"; import { isAbsolute, join } from "node:path"; @@ -91,7 +92,7 @@ describe("parseModelsDevPricing", () => { output_cost_per_million: 20, }; const result = parseModelsDevPricing(payload); - expect(result["model-x"]!.cacheReadPricePerToken).toBe(0); + expect(defined(result["model-x"]).cacheReadPricePerToken).toBe(0); }); test("recurses into nested objects", () => { @@ -162,7 +163,7 @@ describe("lookupModelPricing", () => { }; test("returns pricing for a known model", () => { - expect(lookupModelPricing(cache, "gpt-4")).toEqual(cache.models["gpt-4"]!); + expect(lookupModelPricing(cache, "gpt-4")).toEqual(defined(cache.models["gpt-4"])); }); test("returns null for an unknown model", () => { @@ -228,7 +229,7 @@ describe("loadPricing", () => { }); expect(result).not.toBeNull(); - expect(result!.models["m1"]).toBeDefined(); + expect(defined(result).models["m1"]).toBeDefined(); }); test("falls back to disk cache when fetch fails", async () => { diff --git a/src/prompts.test.ts b/src/prompts.test.ts index dfbb72147..b0d0af30b 100644 --- a/src/prompts.test.ts +++ b/src/prompts.test.ts @@ -23,7 +23,7 @@ const minimalToolDefinitions = [manageTasksDefinition, submitOutputDefinition]; test("buildChatSystemPrompt wires into createChatDirector without error", () => { const prompt = buildChatSystemPrompt(); expect(() => - createChatDirector(prompt, minimalToolDefinitions, { onTasksChange: () => {} }), + createChatDirector(prompt, minimalToolDefinitions, { onTasksChange: () => undefined }), ).not.toThrow(); }); diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 871631724..435310338 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import { ProtocolMismatchError } from "@intx/inference"; import type { ConversationTurn, InferenceOptions } from "@intx/types/runtime"; @@ -117,7 +118,7 @@ describe("openai-compatible adapter reasoning_content handling", () => { test("strips reasoning_content from input messages for DeepSeek models", () => { const assistant = messagesFor("deepseek-v4").find((m) => m["role"] === "assistant"); expect(assistant).toBeDefined(); - expect("reasoning_content" in assistant!).toBe(false); + expect("reasoning_content" in defined(assistant)).toBe(false); }); test("keeps reasoning_content for non-DeepSeek models", () => { diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index a434f0e88..2d66b1865 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -216,7 +216,9 @@ export function clampEffort( if (supported.length === 0) return undefined; if (supported.includes(desired)) return desired; const desiredIdx = REASONING_EFFORTS.indexOf(desired); - let best: ReasoningEffort = supported[0]!; + const first = supported[0]; + if (first === undefined) return undefined; + let best: ReasoningEffort = first; let bestDist = Number.POSITIVE_INFINITY; for (const level of supported) { const dist = Math.abs(REASONING_EFFORTS.indexOf(level) - desiredIdx); diff --git a/src/provider/replay-sanitizer.test.ts b/src/provider/replay-sanitizer.test.ts index cd92f40a5..a800e1c7a 100644 --- a/src/provider/replay-sanitizer.test.ts +++ b/src/provider/replay-sanitizer.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, it } from "bun:test"; import type { AdapterRegistry } from "@intx/inference"; import { createBuiltinRegistry } from "@intx/inference/providers"; @@ -432,6 +433,6 @@ describe("withReplaySanitizer", () => { ); expect(turns.map((t) => t.role)).toEqual(["user", "assistant", "user"]); expect(turns[1]?.content).toEqual([{ type: "text", text: COMPACT_SPACER_TEXT }]); - expect(isHarnessCompactSpacer(turns[1]!)).toBe(true); + expect(isHarnessCompactSpacer(defined(turns[1]))).toBe(true); }); }); diff --git a/src/session/active-host.test.ts b/src/session/active-host.test.ts index 9948672f7..9aecaa94e 100644 --- a/src/session/active-host.test.ts +++ b/src/session/active-host.test.ts @@ -16,20 +16,20 @@ describe("active-host", () => { }); test("returns the handle set by setActiveDisposeHost", () => { - const disposeHost = () => {}; + const disposeHost = () => undefined; setActiveDisposeHost(disposeHost); expect(getActiveDisposeHost()).toBe(disposeHost); }); test("clearActiveDisposeHost removes the handle", () => { - setActiveDisposeHost(() => {}); + setActiveDisposeHost(() => undefined); clearActiveDisposeHost(); expect(getActiveDisposeHost()).toBeNull(); }); test("setActiveDisposeHost overwrites a previously set handle", () => { - setActiveDisposeHost(() => {}); - const second = () => {}; + setActiveDisposeHost(() => undefined); + const second = () => undefined; setActiveDisposeHost(second); expect(getActiveDisposeHost()).toBe(second); }); diff --git a/src/session/assemble-runtime.test.ts b/src/session/assemble-runtime.test.ts index 24751fd27..7cce3a3cf 100644 --- a/src/session/assemble-runtime.test.ts +++ b/src/session/assemble-runtime.test.ts @@ -119,7 +119,7 @@ describe("assembleChatAgent", () => { const fakeStorage = { readBlob: async () => new Uint8Array(), } as unknown as ContextStore & AuditStore; - const fakeAgent = { close: async () => {} } as unknown as Agent; + const fakeAgent = { close: async () => undefined } as unknown as Agent; await withMockedModuleDuring( import.meta.resolve("./optimized-context-store.js"), @@ -173,8 +173,8 @@ describe("assembleChatAgent", () => { computeAdvertised: () => [], activateTools: () => false, inactivityTimeoutMs: 1_000, - onTasksChange: () => {}, - requestContinuation: () => {}, + onTasksChange: () => undefined, + requestContinuation: () => undefined, getProvider: () => ({ providerName: "test", model: "m" }), getWorkdir: () => { workdirCalls.push(liveDir); @@ -199,7 +199,7 @@ describe("assembleChatAgent", () => { compactorCalls.push(liveCompactor.name); return liveCompactor; }, - onBuilt: () => {}, + onBuilt: () => undefined, }); expect(workdirCalls).toEqual([]); diff --git a/src/session/attachment-store.test.ts b/src/session/attachment-store.test.ts index 92f7f0c56..186c7d6a1 100644 --- a/src/session/attachment-store.test.ts +++ b/src/session/attachment-store.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import type { ConversationTurn } from "@intx/types/runtime"; import { @@ -34,8 +35,8 @@ describe("ageImageBlocks / rehydrateAttachmentImages", () => { const aged = await ageImageBlocks(turn); expect(JSON.stringify(aged.turn)).not.toContain(PNG_B64); expect(aged.blobs).toHaveLength(1); - expect(aged.blobs[0]!.contentType).toBe("image/png"); - expect(new TextDecoder().decode(aged.blobs[0]!.bytes)).toBe(PNG_B64); + expect(defined(aged.blobs[0]).contentType).toBe("image/png"); + expect(new TextDecoder().decode(defined(aged.blobs[0]).bytes)).toBe(PNG_B64); const markerText = aged.turn.content.find( (b) => b.type === "text" && b.text.includes("attachment:///"), @@ -48,7 +49,7 @@ describe("ageImageBlocks / rehydrateAttachmentImages", () => { if (bytes === undefined) throw new Error(`Blob not found for key: ${key}`); return bytes; }); - const image = rehydrated[0]!.content.find((b) => b.type === "image"); + const image = defined(rehydrated[0]).content.find((b) => b.type === "image"); expect(image).toEqual({ type: "image", source: { kind: "base64", mimeType: "image/png", data: PNG_B64 }, @@ -77,7 +78,7 @@ describe("ageImageBlocks / rehydrateAttachmentImages", () => { state: {} as never, trigger: "test", }); - expect(result.output[0]!.content.some((b) => b.type === "image")).toBe(true); + expect(defined(result.output[0]).content.some((b) => b.type === "image")).toBe(true); expect(JSON.stringify(result.output)).toContain(PNG_B64); expect(result.record.decisions.restoredImageCount).toBe(1); // Input turn is not mutated — durable history keeps the marker. @@ -101,7 +102,7 @@ describe("ageImageBlocks / rehydrateAttachmentImages", () => { state: {} as never, trigger: "test", }); - expect(result.output[0]!.content).toEqual([{ type: "text", text }]); + expect(defined(result.output[0]).content).toEqual([{ type: "text", text }]); expect(result.record.decisions.restoredImageCount).toBe(0); }); }); diff --git a/src/session/attachment-store.ts b/src/session/attachment-store.ts index aca1b31a3..9f4e6428e 100644 --- a/src/session/attachment-store.ts +++ b/src/session/attachment-store.ts @@ -116,9 +116,13 @@ export function createAttachmentRehydrateTransform( async apply(turns, _ctx) { const output = await rehydrateAttachmentImages(turns, readBlob); let restored = 0; - for (let i = 0; i < turns.length; i++) { - const before = turns[i]!.content.filter((b) => b.type === "image").length; - const after = output[i]!.content.filter((b) => b.type === "image").length; + for (const [i, turn] of turns.entries()) { + const out = output[i]; + if (out === undefined) { + throw new Error("attachment rehydrate length mismatch"); + } + const before = turn.content.filter((b) => b.type === "image").length; + const after = out.content.filter((b) => b.type === "image").length; restored += Math.max(0, after - before); } return { diff --git a/src/session/attachment-uri.ts b/src/session/attachment-uri.ts index c5ebe4ac2..66792d911 100644 --- a/src/session/attachment-uri.ts +++ b/src/session/attachment-uri.ts @@ -42,8 +42,9 @@ export function parseAgedImageMarker(text: string): AgedImageMarker | undefined /^\[image attachment aged: (attachment:\/\/\/[^\s]+) mimeType=([^\s]+) —/, ); if (match === null) return undefined; - const uri = match[1]!; - const mimeType = match[2]!; + const uri = match[1]; + const mimeType = match[2]; + if (uri === undefined || mimeType === undefined) return undefined; const id = parseAttachmentId(uri); if (id === undefined) return undefined; return { uri, id, mimeType }; diff --git a/src/session/compactor.ts b/src/session/compactor.ts index 7605aaa23..383049886 100644 --- a/src/session/compactor.ts +++ b/src/session/compactor.ts @@ -409,8 +409,8 @@ function supersededReadCallIds(pathToReads: ReadonlyMap): Se const successes = reads.filter((r) => !r.isError); if (successes.length < 2) continue; // Newest success (highest order) stays whole; every earlier success stubs. - for (let i = 0; i < successes.length - 1; i++) { - superseded.add(successes[i]!.callId); + for (const read of successes.slice(0, -1)) { + superseded.add(read.callId); } } return superseded; @@ -640,7 +640,8 @@ async function ageImagesOutsideRecentWindow( // Fast path: nothing outside the recent window needs aging. let needsAge = false; for (let i = 0; i < keepFrom; i++) { - if (turns[i]!.content.some((b) => b.type === "image")) { + const turn = turns[i]; + if (turn !== undefined && turn.content.some((b) => b.type === "image")) { needsAge = true; break; } @@ -654,7 +655,8 @@ async function ageImagesOutsideRecentWindow( const out: ConversationTurn[] = []; for (let i = 0; i < turns.length; i++) { - const turn = turns[i]!; + const turn = turns[i]; + if (turn === undefined) continue; if (i < keepFrom && turn.content.some((b) => b.type === "image")) { const aged = await ageImageBlocks(turn); out.push(aged.turn); @@ -751,9 +753,12 @@ export function isHarnessCompactSpacer(turn: ConversationTurn): boolean { // until the next compact inserts one. function frozenPrefixLength(turns: readonly ConversationTurn[]): number { let i = 0; - while (i < turns.length && isCompactedSummaryTurn(turns[i]!)) { + while (i < turns.length) { + const turn = turns[i]; + if (turn === undefined || !isCompactedSummaryTurn(turn)) break; i++; - if (i < turns.length && isHarnessCompactSpacer(turns[i]!)) i++; + const spacer = turns[i]; + if (spacer !== undefined && isHarnessCompactSpacer(spacer)) i++; } return i; } @@ -869,7 +874,10 @@ export function createPruningCompactor(config: Partial = {}): C // Ascending original order keeps the concatenated [anchors, recent] // sequence globally index-ordered, so every result still follows its call. const sortedAnchorIndices = [...anchorIndices].sort((a, b) => a - b); - const anchorTurns = sortedAnchorIndices.map((i) => olderTurns[i]!); + const anchorTurns = sortedAnchorIndices.flatMap((i) => { + const turn = olderTurns[i]; + return turn === undefined ? [] : [turn]; + }); const summarizedTurns = olderTurns.filter((_, i) => !anchorIndices.has(i)); // Keep-set covered the whole live suffix: nothing to fold. Leave the @@ -931,13 +939,17 @@ export function createPruningCompactor(config: Partial = {}): C if (frozenLen === 0) { output = liveOutput; } else { - const lastFrozen = frozen[frozen.length - 1]!; - const firstLive = liveOutput[0]; - const spacer = - lastFrozen.role === "user" && firstLive?.role === "user" - ? [compactSpacerTurn(summaryTurn.timestamp)] - : []; - output = [...frozen, ...spacer, ...liveOutput]; + const lastFrozen = frozen[frozen.length - 1]; + if (lastFrozen === undefined) { + output = liveOutput; + } else { + const firstLive = liveOutput[0]; + const spacer = + lastFrozen.role === "user" && firstLive?.role === "user" + ? [compactSpacerTurn(summaryTurn.timestamp)] + : []; + output = [...frozen, ...spacer, ...liveOutput]; + } } return { diff --git a/src/session/hooks.test.ts b/src/session/hooks.test.ts index 780b1422e..d2afa4548 100644 --- a/src/session/hooks.test.ts +++ b/src/session/hooks.test.ts @@ -31,7 +31,7 @@ function observeOneTurnWithToolResult( describe("createTurnContextCollector tool result truncation", () => { test("retains oversized tool result content within the hook-payload budget", () => { - const collector = createTurnContextCollector(() => {}); + const collector = createTurnContextCollector(() => undefined); const hugeOutput = "x".repeat(HOOK_PAYLOAD_TOOL_RESULT_CHARS * 4); observeOneTurnWithToolResult(collector, hugeOutput); @@ -44,7 +44,7 @@ describe("createTurnContextCollector tool result truncation", () => { }); test("leaves tool result content under the budget untouched", () => { - const collector = createTurnContextCollector(() => {}); + const collector = createTurnContextCollector(() => undefined); const smallOutput = "exit code 0"; observeOneTurnWithToolResult(collector, smallOutput); diff --git a/src/session/hooks.ts b/src/session/hooks.ts index b7491f458..015cd2d66 100644 --- a/src/session/hooks.ts +++ b/src/session/hooks.ts @@ -320,8 +320,8 @@ export function createLifecycleHookManager(args: { // enabled, matching discovery's default before any state was ever saved. initialEnabled?: Record | undefined; }): LifecycleHookManager { - const onEvent = args.onEvent ?? (() => {}); - const logError = args.logError ?? (() => {}); + const onEvent = args.onEvent ?? (() => undefined); + const logError = args.logError ?? (() => undefined); const initialEnabled = args.initialEnabled ?? {}; const statuses = new Map(); for (const hook of args.hooks) { @@ -364,7 +364,7 @@ export function createLifecycleHookManager(args: { if (!status.enabled) continue; pending.push(runHook(status, kind, payload)); } - return Promise.all(pending).then(() => {}); + return Promise.all(pending).then(() => undefined); } onEvent({ type: "hooks.loaded", hooks: snapshot() }); diff --git a/src/session/incremental-jsonl.ts b/src/session/incremental-jsonl.ts index 5353818c6..cab2a8fb1 100644 --- a/src/session/incremental-jsonl.ts +++ b/src/session/incremental-jsonl.ts @@ -170,16 +170,30 @@ export function createSegmentedJSONLWriter( let firstSeg = 0; for (let s = 0; s < prevSegStarts.length; s++) { - if (prevSegStarts[s]! <= prefix) firstSeg = s; + const start = prevSegStarts[s]; + if (start === undefined) break; + if (start <= prefix) firstSeg = s; else break; } - const firstSegStartRecord = prevSegStarts[firstSeg]!; + const firstSegStartRecord = prevSegStarts[firstSeg]; + if (firstSegStartRecord === undefined) { + throw new Error("jsonl first segment start missing"); + } const firstSegEndRecord = prevSegStarts[firstSeg + 1] ?? state?.refs.length ?? 0; - const prevFirstSegBytes = prevOffsets[firstSegEndRecord]! - prevOffsets[firstSegStartRecord]!; + const firstSegEndOffset = prevOffsets[firstSegEndRecord]; + const firstSegStartOffset = prevOffsets[firstSegStartRecord]; + if (firstSegEndOffset === undefined || firstSegStartOffset === undefined) { + throw new Error("jsonl first segment offsets missing"); + } + const prevFirstSegBytes = firstSegEndOffset - firstSegStartOffset; const offsets = prevOffsets.slice(0, prefix + 1); - const keepBytesInFirstSeg = offsets[prefix]! - prevOffsets[firstSegStartRecord]!; + const keepStart = offsets[prefix]; + if (keepStart === undefined) { + throw new Error("jsonl keep offset missing"); + } + const keepBytesInFirstSeg = keepStart - firstSegStartOffset; interface PlanEntry { index: number; @@ -200,9 +214,17 @@ export function createSegmentedJSONLWriter( plan.push({ index: activeIndex, keepBytes: 0, text: "" }); newSegStarts.push(i); } - plan[plan.length - 1]!.text += line; + const last = plan[plan.length - 1]; + if (last === undefined) { + throw new Error("jsonl write plan empty"); + } + last.text += line; currentSegBytes += lineBytes; - offsets.push(offsets[i]! + lineBytes); + const prevOffset = offsets[i]; + if (prevOffset === undefined) { + throw new Error("jsonl offset missing"); + } + offsets.push(prevOffset + lineBytes); } const modifiedPaths: string[] = []; diff --git a/src/session/index.ts b/src/session/index.ts index ffdb701d2..5aea4ca28 100644 --- a/src/session/index.ts +++ b/src/session/index.ts @@ -36,10 +36,15 @@ export function generateSessionId(): string { bytes[5] = ts & 0xff; // Set version to 7 (byte 6, high nibble) - bytes[6] = (bytes[6]! & 0x0f) | 0x70; + const versionByte = bytes[6]; + const variantByte = bytes[8]; + if (versionByte === undefined || variantByte === undefined) { + throw new Error("uuid v7 bytes missing"); + } + bytes[6] = (versionByte & 0x0f) | 0x70; // Set variant to 10xx (byte 8, high nibble) - bytes[8] = (bytes[8]! & 0x3f) | 0x80; + bytes[8] = (variantByte & 0x3f) | 0x80; // Format as hex string with dashes const hex = Array.from(bytes) diff --git a/src/session/optimized-context-store.test.ts b/src/session/optimized-context-store.test.ts index a1762051c..247be1cd1 100644 --- a/src/session/optimized-context-store.test.ts +++ b/src/session/optimized-context-store.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, test, expect } from "bun:test"; import fs from "node:fs"; import os from "node:os"; @@ -69,7 +70,7 @@ describe("createOptimizedContextStore load", () => { const loaded = await store.load(); expect(loaded.turns).toHaveLength(1); - expect((loaded.turns[0]!.content[0] as { text: string }).text).toBe("only"); + expect((defined(loaded.turns[0]).content[0] as { text: string }).text).toBe("only"); }); test("recovers from a torn final line in the active segment", async () => { @@ -213,7 +214,7 @@ describe("createOptimizedContextStore load", () => { const loaded = await store.load(); expect(loaded.turns).toHaveLength(1); - expect((loaded.turns[0]!.content[0] as { text: string }).text).toBe("kept"); + expect((defined(loaded.turns[0]).content[0] as { text: string }).text).toBe("kept"); expect(loaded.pendingOperations).toEqual([]); expect(loaded.connectorState).toBeNull(); }); @@ -369,8 +370,8 @@ describe("createOptimizedContextStore load", () => { content: [{ type: "text", text: "[Compacted prior context]\nsummary" }], timestamp: 1, }, - history[history.length - 2]!, - history[history.length - 1]!, + defined(history[history.length - 2]), + defined(history[history.length - 1]), ]; await store2.writeTurns(compacted); await store2.writeMetadata({ @@ -543,7 +544,7 @@ describe("createOptimizedContextStore checkpoint", () => { const loaded = await reloaded.load(); expect(loaded.turns).toHaveLength(total); - const head = (await store.log(1))[0]!; + const head = defined((await store.log(1))[0]); const atHead = await store.readAt(head.hash); expect(atHead).toHaveLength(total); }, 20_000); diff --git a/src/session/optimized-context-store.ts b/src/session/optimized-context-store.ts index 7d8bdec83..444868968 100644 --- a/src/session/optimized-context-store.ts +++ b/src/session/optimized-context-store.ts @@ -131,7 +131,8 @@ function parseSegmentTurns( const turns: ConversationTurn[] = []; for (let i = 0; i < lines.length; i++) { - const line = lines[i]!; + const line = lines[i]; + if (line === undefined) continue; if (line.length === 0) continue; const isLast = i === lines.length - 1; let raw: unknown; @@ -287,7 +288,8 @@ export async function loadRecentTurns(dir: string, minTurns: number): Promise= 0; i--) { - const name = segments[i]!; + const name = segments[i]; + if (name === undefined) continue; const text = await fs.promises.readFile(path.join(dir, name), "utf-8"); // Only the active (last) segment can be mid-write; sealed ones are complete. // Display-only: skip lines that will not parse rather than losing the whole @@ -301,8 +303,10 @@ export async function loadRecentTurns(dir: string, minTurns: number): Promise= 0; i--) - turns.push(...collectedNewestFirst[i]!); + for (let i = collectedNewestFirst.length - 1; i >= 0; i--) { + const chunk = collectedNewestFirst[i]; + if (chunk !== undefined) turns.push(...chunk); + } return turns; } diff --git a/src/session/run-sink.test.ts b/src/session/run-sink.test.ts index 436284528..604c49e5d 100644 --- a/src/session/run-sink.test.ts +++ b/src/session/run-sink.test.ts @@ -13,7 +13,7 @@ function event(type: string, data: unknown): ReactorEmittedEvent { function stubHookManager(statuses: LifecycleHookStatus[]) { return { getStatuses: () => statuses, - dispatchPostTurn: () => {}, + dispatchPostTurn: () => undefined, }; } @@ -34,8 +34,8 @@ function attributionHarness(selectedSource = { provider: "provider-a", model: "m captured.push({ event: capturedEvent, properties }); }, captureIntentional: () => false, - flush: async () => {}, - discard: () => {}, + flush: async () => undefined, + discard: () => undefined, }; const observer = createTurnObserver({ telemetry: () => telemetry, diff --git a/src/session/runtime-assembly.ts b/src/session/runtime-assembly.ts index 8a6d0a4a9..ce892fb24 100644 --- a/src/session/runtime-assembly.ts +++ b/src/session/runtime-assembly.ts @@ -222,9 +222,9 @@ export function skillDirsFromEnabledPlugins( modules: readonly PluginModule[], pluginConfig: Record, ): string[] { - return modules - .filter((m) => m.dir !== undefined && isPluginModuleEnabled(m, pluginConfig)) - .map((m) => m.dir!); + return modules.flatMap((m) => + m.dir !== undefined && isPluginModuleEnabled(m, pluginConfig) ? [m.dir] : [], + ); } // --------------------------------------------------------------------------- diff --git a/src/session/state.ts b/src/session/state.ts index 419095652..852ce990c 100644 --- a/src/session/state.ts +++ b/src/session/state.ts @@ -94,7 +94,7 @@ export async function saveState( // Swallow the error in the chain tail (not in `write`, which still rejects // for this caller) so one failed save doesn't permanently wedge later // saves for the same session. - const tail = write.catch(() => {}); + const tail = write.catch(() => undefined); writeChains.set(sessionId, tail); // Once this is the last write for the session, drop the entry so a // long-lived process doesn't retain a chain per session forever. diff --git a/src/session/stream-consumer.test.ts b/src/session/stream-consumer.test.ts index 28f8fd311..7413000ec 100644 --- a/src/session/stream-consumer.test.ts +++ b/src/session/stream-consumer.test.ts @@ -25,7 +25,7 @@ describe("consumeStream", () => { return true; }) as typeof process.stderr.write; try { - await consumeStream(eventsThenError(), () => {}); + await consumeStream(eventsThenError(), () => undefined); } finally { process.stderr.write = original; } diff --git a/src/session/stream-journal.test.ts b/src/session/stream-journal.test.ts index b5623e78b..3f5a62b87 100644 --- a/src/session/stream-journal.test.ts +++ b/src/session/stream-journal.test.ts @@ -155,7 +155,7 @@ describe("createCycleTextRecorder", () => { const recorder = createCycleTextRecorder(() => dir); recorder.handleEvent(delta("buffered text")); - let resolveDrain: () => void = () => {}; + let resolveDrain: () => void = () => undefined; const drain = new Promise((resolve) => { resolveDrain = resolve; }); diff --git a/src/settings.test.ts b/src/settings.test.ts index 472386099..f5a3dba71 100644 --- a/src/settings.test.ts +++ b/src/settings.test.ts @@ -717,7 +717,7 @@ describe("loaders", () => { expect(loaded?.providers["go/personal"]?.opencodeGo).toBe(true); expect(loaded?.providers["go/personal"]?.baseURL).toBe(OPENCODE_GO_BASE_URL); } finally { - await chmod(dir, 0o755).catch(() => {}); + await chmod(dir, 0o755).catch(() => undefined); await rm(dir, { recursive: true, force: true }); } }); diff --git a/src/shell/background-shell.test.ts b/src/shell/background-shell.test.ts index d9d8e1db0..b7d0f42e7 100644 --- a/src/shell/background-shell.test.ts +++ b/src/shell/background-shell.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"; @@ -83,9 +84,9 @@ describe("background shell registry", () => { await registry.collect(started.id, 5_000); } expect(ids).toHaveLength(MAX_COMPLETED_BACKGROUND_SHELLS + 1); - const evicted = await registry.collect(ids[0]!, 0); + const evicted = await registry.collect(defined(ids[0]), 0); expect(evicted.state).toBe("not-found"); - const retained = await registry.collect(ids[ids.length - 1]!, 0); + const retained = await registry.collect(defined(ids[ids.length - 1]), 0); expect(retained.state).toBe("completed"); }); diff --git a/src/shell/persistent-shell-cwd.ts b/src/shell/persistent-shell-cwd.ts index f9b7a8cbd..ad23388bb 100644 --- a/src/shell/persistent-shell-cwd.ts +++ b/src/shell/persistent-shell-cwd.ts @@ -26,7 +26,8 @@ export function parsePwdProbeOutput(raw: string): PwdProbeParse { let finalCwd: string | undefined; let markerIndex = -1; for (let i = lines.length - 1; i >= 0; i--) { - const line = lines[i]!; + const line = lines[i]; + if (line === undefined) continue; if (line.startsWith(SHELL_PWD_MARKER)) { markerIndex = i; const path = line.slice(SHELL_PWD_MARKER.length).trim(); diff --git a/src/shell/run-shell-authz.ts b/src/shell/run-shell-authz.ts index d43ca601f..9fe5721a4 100644 --- a/src/shell/run-shell-authz.ts +++ b/src/shell/run-shell-authz.ts @@ -3,6 +3,20 @@ import { splitChainedCommand, tokenize } from "../permission/command.js"; +function skipMatching( + tokens: readonly string[], + start: number, + pred: (token: string) => boolean, +): number { + let i = start; + while (i < tokens.length) { + const token = tokens[i]; + if (token === undefined || !pred(token)) break; + i++; + } + return i; +} + // A command-position anchor: the start of the command, or immediately after a // shell separator or subshell open, optionally preceded by a run of NAME=value // environment assignments (so `X=1 sudo …` is still recognised as `sudo` in @@ -147,7 +161,8 @@ function pipelineHeads(command: string): string[] { }; for (let i = 0; i < command.length; i++) { - const ch = command[i]!; + const ch = command[i]; + if (ch === undefined) break; if (quote !== undefined) { if (ch === quote) quote = undefined; if (!headClosed) head += ch; @@ -184,9 +199,8 @@ function pipelineHeads(command: string): string[] { // (e.g. `grep 'a b'` has one operand, not two). export function tokenizeSegment(segment: string): string[] { const tokens = tokenize(segment); - let i = 0; - while (i < tokens.length && ENV_ASSIGNMENT.test(tokens[i]!)) i++; - while (i < tokens.length && RM_WRAPPER.test(tokens[i]!)) i++; + let i = skipMatching(tokens, 0, (t) => ENV_ASSIGNMENT.test(t)); + i = skipMatching(tokens, i, (t) => RM_WRAPPER.test(t)); return tokens.slice(i); } @@ -196,7 +210,8 @@ export function tokenizeSegment(segment: string): string[] { function fileOperandCount(args: string[], valueFlags: Set): number { let count = 0; for (let i = 0; i < args.length; i++) { - const arg = args[i]!; + const arg = args[i]; + if (arg === undefined) continue; if (arg === "--") continue; if (arg.startsWith("-")) { if (valueFlags.has(arg)) i++; @@ -414,7 +429,8 @@ const SHELL_SEPARATE_VALUE_FLAGS = new Set(["-O", "-o"]); function peelShellDashC(tokens: string[], start: number): PeelOutcome { let i = start; while (i < tokens.length) { - const t = tokens[i]!; + const t = tokens[i]; + if (t === undefined) break; if (t === "--") { i++; break; @@ -458,7 +474,8 @@ function peelShellDashC(tokens: string[], start: number): PeelOutcome { function peelXargs(tokens: string[], start: number): PeelOutcome { let i = start; while (i < tokens.length) { - const t = tokens[i]!; + const t = tokens[i]; + if (t === undefined) break; if (t === "--") { i++; break; @@ -470,7 +487,10 @@ function peelXargs(tokens: string[], start: number): PeelOutcome { } if (XARGS_VALUE_FLAGS.has(t)) { i++; - if (i < tokens.length && !tokens[i]!.startsWith("-")) i++; + if (i < tokens.length) { + const next = tokens[i]; + if (next !== undefined && !next.startsWith("-")) i++; + } continue; } // Clustered short options; -I/-i/-n/… with glued values are treated as one token. @@ -519,7 +539,10 @@ function advancePastEnvValueFlag(tokens: string[], i: number): number | null { if (t === undefined) return null; if (ENV_VALUE_FLAGS.has(t)) { let j = i + 1; - if (j < tokens.length && !tokens[j]!.startsWith("-")) j++; + if (j < tokens.length) { + const next = tokens[j]; + if (next !== undefined && !next.startsWith("-")) j++; + } return j; } if (isEnvValueEqualsFlag(t)) return i + 1; @@ -539,7 +562,8 @@ function expandEnvSplitSeparators(payload: string): string | null { let out = ""; let quote: "'" | '"' | null = null; for (let i = 0; i < payload.length; i++) { - const c = payload[i]!; + const c = payload[i]; + if (c === undefined) break; if (quote === "'") { out += c; if (c === "'") quote = null; @@ -579,7 +603,7 @@ function peelEnvSplitUtility(command: string): PeelOutcome { if (expanded === null || isOpaquePayload(expanded)) return { kind: "opaque" }; const tokens = tokenize(expanded); let i = 0; - while (i < tokens.length && ENV_ASSIGNMENT.test(tokens[i]!)) i++; + i = skipMatching(tokens, i, (t) => ENV_ASSIGNMENT.test(t)); while (i < tokens.length && (tokens[i] === "--" || tokens[i] === "-")) i++; i = skipEnvFlagsAndAssignments(tokens, i); if (i >= tokens.length) return { kind: "opaque" }; @@ -625,7 +649,8 @@ function finishEnvSplitPayload(payload: string, tokens: string[], restStart: num function peelEnvSplitString(tokens: string[], start: number): PeelOutcome { let i = start; while (i < tokens.length) { - const t = tokens[i]!; + const t = tokens[i]; + if (t === undefined) break; if (t === "--") return { kind: "none" }; // --split-string=PAYLOAD @@ -691,7 +716,8 @@ function peelEnvSplitString(tokens: string[], start: number): PeelOutcome { function skipEnvFlagsAndAssignments(tokens: string[], start: number): number { let i = start; while (i < tokens.length) { - const t = tokens[i]!; + const t = tokens[i]; + if (t === undefined) break; if (t === "--") return i + 1; if (ENV_ASSIGNMENT.test(t)) { i++; @@ -716,11 +742,13 @@ function skipEnvFlagsAndAssignments(tokens: string[], start: number): number { function peelOnce(segment: string): PeelOutcome { const tokens = tokenize(segment); let i = 0; - while (i < tokens.length && ENV_ASSIGNMENT.test(tokens[i]!)) i++; + i = skipMatching(tokens, i, (t) => ENV_ASSIGNMENT.test(t)); let strippedPrefix = false; while (i < tokens.length) { - const base = programBasename(tokens[i]!); + const current = tokens[i]; + if (current === undefined) break; + const base = programBasename(current); if (base === "env") { // Prefer split-string peel: the whole payload is one quoted argument // that env re-splits itself, so the transparent-prefix path below @@ -736,7 +764,8 @@ function peelOnce(segment: string): PeelOutcome { i++; // Optional duration (10, 30s, 1m, …) and common long/short flags. while (i < tokens.length) { - const t = tokens[i]!; + const t = tokens[i]; + if (t === undefined) break; if (/^\d/.test(t)) { i++; continue; @@ -752,7 +781,10 @@ function peelOnce(segment: string): PeelOutcome { t.startsWith("--signal=") ) { i++; - if (!t.includes("=") && i < tokens.length && !tokens[i]!.startsWith("-")) i++; + if (!t.includes("=") && i < tokens.length) { + const next = tokens[i]; + if (next !== undefined && !next.startsWith("-")) i++; + } continue; } i++; @@ -772,7 +804,9 @@ function peelOnce(segment: string): PeelOutcome { if (i >= tokens.length) return strippedPrefix ? { kind: "opaque" } : { kind: "none" }; - const prog = programBasename(tokens[i]!); + const current = tokens[i]; + if (current === undefined) return strippedPrefix ? { kind: "opaque" } : { kind: "none" }; + const prog = programBasename(current); if (SHELL_INTERPRETERS.has(prog)) { // A backtick or `$(` anywhere in the raw segment means the -c payload may // contain command substitution. tokenize() surfaces substitution content as @@ -818,7 +852,7 @@ export interface ShellExpandResult { // Assignments before the marker are preserved (`FOO=1 -- find /` → `FOO=1 find /`). function dropLeadingEndOfOptionsTokens(tokens: string[]): string[] { let i = 0; - while (i < tokens.length && ENV_ASSIGNMENT.test(tokens[i]!)) i++; + i = skipMatching(tokens, i, (t) => ENV_ASSIGNMENT.test(t)); const head = tokens.slice(0, i); while (i < tokens.length && (tokens[i] === "--" || tokens[i] === "-")) i++; return head.concat(tokens.slice(i)); @@ -883,9 +917,9 @@ function segmentRmArgs(segment: string): string[] | undefined { // Quote-aware: env -S payloads often carry quoted flags (`rm '-rf' '/'`). const tokens = tokenize(segment); let i = 0; - while (i < tokens.length && ENV_ASSIGNMENT.test(tokens[i]!)) i++; + i = skipMatching(tokens, i, (t) => ENV_ASSIGNMENT.test(t)); while (i < tokens.length && (tokens[i] === "--" || tokens[i] === "-")) i++; - while (i < tokens.length && RM_WRAPPER.test(tokens[i]!)) i++; + i = skipMatching(tokens, i, (t) => RM_WRAPPER.test(t)); while (i < tokens.length && (tokens[i] === "--" || tokens[i] === "-")) i++; if (programBasename(tokens[i] ?? "") !== "rm") return undefined; return tokens.slice(i + 1); @@ -931,7 +965,8 @@ function skipQuotedSpans(command: string): string { }; for (let i = 0; i < command.length; i++) { - const ch = command[i]!; + const ch = command[i]; + if (ch === undefined) break; if (quote === "'") { if (ch === "'") { quote = undefined; diff --git a/src/telemetry/ai-observability.test.ts b/src/telemetry/ai-observability.test.ts index c119b175c..4d6246fef 100644 --- a/src/telemetry/ai-observability.test.ts +++ b/src/telemetry/ai-observability.test.ts @@ -34,8 +34,8 @@ function fakeTelemetry(): { captured.push({ event, properties }); }, captureIntentional: () => false, - flush: async () => {}, - discard: () => {}, + flush: async () => undefined, + discard: () => undefined, }; return { telemetry, captured }; } diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index e477fe5cf..4ecc85501 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -313,10 +313,10 @@ export interface Telemetry { export const NOOP_TELEMETRY: Telemetry = { enabled: false, installationId: "", - capture: () => {}, + capture: () => undefined, captureIntentional: () => false, - flush: async () => {}, - discard: () => {}, + flush: async () => undefined, + discard: () => undefined, }; // Fire-and-forget PostHog batch client. Never throws, never blocks the diff --git a/src/upgrade/index.test.ts b/src/upgrade/index.test.ts index 59c034012..0f20fe04f 100644 --- a/src/upgrade/index.test.ts +++ b/src/upgrade/index.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { @@ -339,7 +340,7 @@ describe("scheduleUpgradeNotice", () => { method: "unknown", }, }); - resolveFetch!("0.2.0"); + defined(resolveFetch)("0.2.0"); await fetchP; await Promise.resolve(); await Promise.resolve(); diff --git a/src/web/plugin-provider.test.ts b/src/web/plugin-provider.test.ts index 67496f4b2..911a1640b 100644 --- a/src/web/plugin-provider.test.ts +++ b/src/web/plugin-provider.test.ts @@ -1,3 +1,4 @@ +import { defined } from "../../tests/helpers/defined.js"; import { describe, expect, test } from "bun:test"; import { collectWebPlugins, @@ -38,7 +39,7 @@ describe("collectWebPlugins", () => { ]; const candidates = collectWebPlugins(modules); expect(candidates.map((c) => c.id)).toEqual(["exa"]); - expect(candidates[0]!.credentials[0]!.key).toBe("apiKey"); + expect(defined(defined(candidates[0]).credentials[0]).key).toBe("apiKey"); }); }); diff --git a/src/web/secret-scrub.ts b/src/web/secret-scrub.ts index 02f639d29..5059a59e5 100644 --- a/src/web/secret-scrub.ts +++ b/src/web/secret-scrub.ts @@ -41,8 +41,9 @@ function redactPattern(text: string, pattern: RegExp): string { // "key":"value" has at least 4 quotes (open/close for key, open/close for value). if (quotes.length >= 4) { - const valueOpen = quotes[2]!; - const valueClose = quotes[quotes.length - 1]!; + const valueOpen = quotes[2]; + const valueClose = quotes[quotes.length - 1]; + if (valueOpen === undefined || valueClose === undefined) return "[REDACTED]"; return match.slice(0, valueOpen + 1) + "[REDACTED]" + match.slice(valueClose); } diff --git a/src/workflows/coordinator.ts b/src/workflows/coordinator.ts index eec1c59bd..7db1c6224 100644 --- a/src/workflows/coordinator.ts +++ b/src/workflows/coordinator.ts @@ -12,7 +12,7 @@ export class WorkflowCoordinator { // Persist runtime state after every transition so a run can resume // mid-recipe. Failures are swallowed — losing the workflow checkpoint must // not crash the agent loop. - private readonly persist: () => void = () => {}, + private readonly persist: () => void = () => undefined, // When true the workflow pauses after each step for user confirmation; the // directive tells the agent to gate via ask_operator before advancing. private readonly stepThrough = false, diff --git a/tests/fixtures/crash-run/simulate-run-end-crash.ts b/tests/fixtures/crash-run/simulate-run-end-crash.ts index 6adec7932..e64e0f7c5 100644 --- a/tests/fixtures/crash-run/simulate-run-end-crash.ts +++ b/tests/fixtures/crash-run/simulate-run-end-crash.ts @@ -37,7 +37,7 @@ process.stdout.write(`${sessionDir(cwd, sessionId)}\n`); // write is still in flight" without needing to release it: whether the // process observes "done" or "crashed" is decided before this write would // ever land. -setTestWriteGate(new Promise(() => {})); +setTestWriteGate(new Promise(() => undefined)); // Fire the run-end write the same way writeRunSnapshot does for a terminal // status, but don't await it — runner.ts doesn't either from the crash diff --git a/tests/fixtures/crash-run/simulate-signal.ts b/tests/fixtures/crash-run/simulate-signal.ts index 35c27ccbe..b0633ce55 100644 --- a/tests/fixtures/crash-run/simulate-signal.ts +++ b/tests/fixtures/crash-run/simulate-signal.ts @@ -58,4 +58,4 @@ const poll = setInterval(() => { if (typeof poll.unref === "function") poll.unref(); // Keep the event loop alive until the test sends a signal. -setInterval(() => {}, 60_000); +setInterval(() => undefined, 60_000); diff --git a/tests/fixtures/plugins/exa/src/index.test.ts b/tests/fixtures/plugins/exa/src/index.test.ts index 44a153ed7..f09ee750d 100644 --- a/tests/fixtures/plugins/exa/src/index.test.ts +++ b/tests/fixtures/plugins/exa/src/index.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import createWebProvider from "./index.js"; +import { defined } from "../../../../helpers/defined.js"; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { @@ -45,10 +46,11 @@ describe("search", () => { const results = await provider.search("test", new AbortController().signal); expect(results.length).toBe(1); - expect(results[0]!.title).toBe("Result One"); - expect(results[0]!.url).toBe("https://example.com/1"); - expect(results[0]!.snippet).toBe("Snippet one"); - expect(results[0]!.extra).toEqual({ + const first = defined(results[0], "search result"); + expect(first.title).toBe("Result One"); + expect(first.url).toBe("https://example.com/1"); + expect(first.snippet).toBe("Snippet one"); + expect(first.extra).toEqual({ publishedDate: "2024-01-01", author: "Author A", score: 0.9, @@ -65,7 +67,7 @@ describe("search", () => { const results = await provider.search("test", new AbortController().signal); expect(results.length).toBe(1); - expect(results[0]!.snippet).toBe(""); + expect(defined(results[0], "search result").snippet).toBe(""); }); test("throws on non-ok response", async () => { diff --git a/tests/fixtures/tier-xhard/src/notify.ts b/tests/fixtures/tier-xhard/src/notify.ts index c6ef5a444..06374dd35 100644 --- a/tests/fixtures/tier-xhard/src/notify.ts +++ b/tests/fixtures/tier-xhard/src/notify.ts @@ -3,7 +3,7 @@ import { declareTable, put, all } from "./store.ts"; export const MAX_ATTEMPTS = 3; /** Delivery sink. Tests replace this to simulate failures. */ -export let deliver: (orderId: string) => Promise = async () => {}; +export let deliver: (orderId: string) => Promise = async () => undefined; export function setDeliver(fn: (orderId: string) => Promise): void { deliver = fn; } diff --git a/tests/fixtures/tier-xhard/tests/notify.test.ts b/tests/fixtures/tier-xhard/tests/notify.test.ts index 0a5d38b1f..5a952109a 100644 --- a/tests/fixtures/tier-xhard/tests/notify.test.ts +++ b/tests/fixtures/tier-xhard/tests/notify.test.ts @@ -13,7 +13,7 @@ import { describe("order notifications", () => { beforeEach(() => { reset(); - setDeliver(async () => {}); + setDeliver(async () => undefined); }); test("a queued notification is delivered", async () => { diff --git a/tests/integration/harness.ts b/tests/integration/harness.ts index 3d633c65f..e8b00811b 100644 --- a/tests/integration/harness.ts +++ b/tests/integration/harness.ts @@ -79,7 +79,7 @@ export async function openIntegrationSession( configSchema: type({}), factory: (_config, _env, agentCtx) => createChatDirector(agentCtx.systemPrompt, [...agentCtx.toolDefinitions], { - onTasksChange: () => {}, + onTasksChange: () => undefined, inactivityTimeoutMs: 750_000, }), }); @@ -202,7 +202,7 @@ export async function runUntilSuspended( message: string, ): Promise { const events: ReactorEmittedEvent[] = []; - let resolveReply: (text: string) => void = () => {}; + let resolveReply: (text: string) => void = () => undefined; const replyPromise = new Promise((resolve) => { resolveReply = resolve; }); diff --git a/tests/integration/reactor-approval-suspend.test.ts b/tests/integration/reactor-approval-suspend.test.ts index 11a0104c5..2ded0ce86 100644 --- a/tests/integration/reactor-approval-suspend.test.ts +++ b/tests/integration/reactor-approval-suspend.test.ts @@ -14,6 +14,7 @@ import { runUntilSuspended, toolDoneEvents, } from "./harness.js"; +import { defined } from "../helpers/defined.js"; const CURL_CALL = { name: "run_shell", args: { command: "curl -sS https://example.com" } }; @@ -201,8 +202,9 @@ describe("integration — reactor approval suspend/resume", () => { await turn.reply(); const dones = toolDoneEvents(turn.events); expect(dones.length).toBeGreaterThanOrEqual(1); - expect(dones[0]!.data.result.isError).toBe(true); - expect(dones[0]!.data.result.content).toContain("Denied by policy: tool:run_shell/invoke"); + const denied = defined(dones[0], "tool done event"); + expect(denied.data.result.isError).toBe(true); + expect(denied.data.result.content).toContain("Denied by policy: tool:run_shell/invoke"); } finally { await closeIntegrationSession(session); } @@ -222,10 +224,11 @@ describe("integration — reactor approval suspend/resume", () => { await turn.reply(); const dones = toolDoneEvents(turn.events); expect(dones.length).toBeGreaterThanOrEqual(1); - expect(dones[0]!.data.result.isError).toBe(true); + const denied = defined(dones[0], "tool done event"); + expect(denied.data.result.isError).toBe(true); // Assert the block text: this deny is a policy deny, not an operator // decline, so the director's classification must leave it unmatched. - expect(dones[0]!.data.result.content).toContain("Denied by policy:"); + expect(denied.data.result.content).toContain("Denied by policy:"); // Stricter-than-authz command deny is preserved as a block effect; the // approval surface was never raised. expect(ctx.asks.length).toBe(0); @@ -286,7 +289,7 @@ describe("authz seam carries the ToolCall context", () => { name: "run_shell", arguments: { command: "ls" }, }); - expect(seen[0]!.arguments).toEqual({ command: "ls" }); + expect(defined(seen[0], "seen tool call").arguments).toEqual({ command: "ls" }); }); test("parallel batch keeps per-call attribution", async () => { @@ -301,7 +304,7 @@ describe("authz seam carries the ToolCall context", () => { }); test("non-ToolCall context fails loud", async () => { - const authorize = extWith(() => {}); + const authorize = extWith(() => undefined); await expect(authorize("tool:run_shell", "invoke", { nope: true })).rejects.toThrow( /not a ToolCall/, ); diff --git a/tests/unit/agent-context-extensions.test.ts b/tests/unit/agent-context-extensions.test.ts index 49945a111..61e1fd485 100644 --- a/tests/unit/agent-context-extensions.test.ts +++ b/tests/unit/agent-context-extensions.test.ts @@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { loadAgentContextExtensions } from "../../src/agent/context-extensions.js"; +import { defined } from "../helpers/defined.js"; let dir: string; @@ -22,7 +23,7 @@ test("AGENTS.md present and non-empty returns content framed as reference", asyn expect(result[0]).toContain("AGENTS.md"); // Framed as reference so the agent does not execute its onboarding steps. expect(result[0]).toContain("Do not execute"); - expect(result[0]!.endsWith(content)).toBe(true); + expect(defined(result[0], "AGENTS.md content").endsWith(content)).toBe(true); }); test("AGENTS.md absent returns empty array without throwing", async () => { diff --git a/tests/unit/agent-tools.test.ts b/tests/unit/agent-tools.test.ts index c459aaed6..40e673309 100644 --- a/tests/unit/agent-tools.test.ts +++ b/tests/unit/agent-tools.test.ts @@ -13,7 +13,7 @@ test("createAgentToolset wires posix tools for a real cwd", async () => { spyOn(posixModule, "createPosixTools").mockReturnValue({ definitions: [], run: async () => ({ output: "" }), - dispose: async () => {}, + dispose: async () => undefined, } as unknown as ReturnType); const { createAgentToolset } = await import("../../src/agent/tools.js"); diff --git a/tests/unit/codex-sse-fixtures.test.ts b/tests/unit/codex-sse-fixtures.test.ts index 103288cbb..b7ee44125 100644 --- a/tests/unit/codex-sse-fixtures.test.ts +++ b/tests/unit/codex-sse-fixtures.test.ts @@ -13,6 +13,7 @@ import { tagSignature, } from "../../src/provider/codex-responses-adapter.js"; import type { InferenceEvent, LastCycleSource } from "@intx/types/runtime"; +import { defined } from "../helpers/defined.js"; import { ProtocolMismatchError } from "@intx/inference"; const SOURCE: LastCycleSource = { @@ -129,7 +130,10 @@ describe("codex-sse fixtures (golden parse)", () => { }); // Terminal event in the fixture is response.completed. - const lastPayload = loadFixture("interleaved-reasoning-text-tools.json").at(-1)!; + const lastPayload = defined( + loadFixture("interleaved-reasoning-text-tools.json").at(-1), + "last payload", + ); expect(isResponsesStreamTerminal(JSON.stringify(lastPayload))).toBe(true); }); @@ -148,7 +152,7 @@ describe("codex-sse fixtures (golden parse)", () => { // response.incomplete is intentionally not mapped to usage (completed-only). expect(out.some((e) => e.type === "inference.usage")).toBe(false); - const lastPayload = loadFixture("incomplete.json").at(-1)!; + const lastPayload = defined(loadFixture("incomplete.json").at(-1), "last payload"); expect(isResponsesStreamTerminal(JSON.stringify(lastPayload))).toBe(true); }); @@ -177,7 +181,7 @@ describe("codex-sse fixtures (golden parse)", () => { expect(out).toEqual([]); // response.done is a terminal alias even when it yields no payload events. - const lastPayload = loadFixture("lifecycle-ignored.json").at(-1)!; + const lastPayload = defined(loadFixture("lifecycle-ignored.json").at(-1), "last payload"); expect((lastPayload as { type: string }).type).toBe("response.done"); expect(isResponsesStreamTerminal(JSON.stringify(lastPayload))).toBe(true); }); diff --git a/tests/unit/compactor-pairing.test.ts b/tests/unit/compactor-pairing.test.ts index f4bb1b73a..be51c5a3b 100644 --- a/tests/unit/compactor-pairing.test.ts +++ b/tests/unit/compactor-pairing.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import type { ConversationTurn } from "@intx/types/runtime"; import { createPruningCompactor, buildTurnSummary } from "../../src/session/compactor.js"; import { assertWellFormedToolSequence } from "@intx/inference"; +import { defined } from "../helpers/defined.js"; // The runtime puts a tool_call on an assistant turn and its tool_result on the // FOLLOWING user turn, so the two halves of a pair can land on opposite sides of @@ -239,7 +240,7 @@ describe("pruning compactor stubs superseded file reads (CL-4374)", () => { expect(older).toMatch(/read_file/); expect(older).toMatch(/src\/hot\.ts/); expect(older).toMatch(/omitted|chars/); - expect(older!.length).toBeLessThan(oldBody.length); + expect(defined(older, "older body").length).toBeLessThan(oldBody.length); }); test("preserves error read results verbatim even when a later success supersedes the path", async () => { diff --git a/tests/unit/corbits-skills-catalog.test.ts b/tests/unit/corbits-skills-catalog.test.ts index 058a8ca2b..db9d890c2 100644 --- a/tests/unit/corbits-skills-catalog.test.ts +++ b/tests/unit/corbits-skills-catalog.test.ts @@ -3,6 +3,7 @@ import { readdir } from "node:fs/promises"; import { join } from "node:path"; import { expect, test } from "bun:test"; import { loadSkillCommands } from "../../src/plugins/skill-commands.js"; +import { defined } from "../helpers/defined.js"; const pluginRoot = join(import.meta.dirname, "../../plugins/corbits-skills"); @@ -201,7 +202,7 @@ test("Corbits-only skills do not contain GaaS tool names", async () => { test("loadSkillCommands lists exactly the nine slash actions", async () => { const cmds = await loadSkillCommands(join(import.meta.dirname, "../../plugins/corbits-skills")); - expect(cmds!.map((c) => c.name).sort()).toEqual([ + expect(defined(cmds, "skill commands").map((c) => c.name).sort()).toEqual([ "ast-grep", "create-issue", "implement", diff --git a/tests/unit/data-only-agent.test.ts b/tests/unit/data-only-agent.test.ts index 4441d8093..dba524a18 100644 --- a/tests/unit/data-only-agent.test.ts +++ b/tests/unit/data-only-agent.test.ts @@ -2,8 +2,12 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test"; import { mkdir, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import { type } from "arktype"; import { loadDataOnlyAgentPlugin } from "../../src/plugins/data-only-agent.js"; -import type { AgentProfile } from "../../src/agent/profile-types.js"; +import type { DataOnlyAgentPlugin } from "../../src/plugins/data-only-agent.js"; +import { AgentProfileSchema } from "../../src/agent/profiles.js"; +import type { CapabilityFilter, InferenceLeg, InferenceSpec } from "../../src/agent/profile-types.js"; +import { defined } from "../helpers/defined.js"; let root: string; @@ -31,6 +35,14 @@ async function mkdtemp(): Promise { return dir; } +function firstAgent(plugin: DataOnlyAgentPlugin) { + const parsed = AgentProfileSchema(defined(plugin.agentPlugin.agents[0], "agent")); + if (parsed instanceof type.errors) { + throw new Error(`expected agent to match AgentProfileSchema: ${parsed.summary}`); + } + return parsed; +} + describe("loadDataOnlyAgentPlugin", () => { test("returns null when there are no *.md files (neither in agents/ nor at root)", async () => { const dir = await makePlugin({ README: "hi", "notes.txt": "no" }); @@ -48,15 +60,17 @@ describe("loadDataOnlyAgentPlugin", () => { const dir = await makePlugin({ "agents/karen.md": "You orchestrate.\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "team" }); - expect(plugin).not.toBeNull(); - expect(plugin!.manifest).toEqual({ + const plugin = defined( + await loadDataOnlyAgentPlugin(dir, { pluginId: "team" }), + "plugin", + ); + expect(plugin.manifest).toEqual({ id: "team", name: "team", kind: "agent", }); - expect(plugin!.agentPlugin.agents.length).toBe(1); - const agent = plugin!.agentPlugin.agents[0] as { id: string; systemPromptRole: string }; + expect(plugin.agentPlugin.agents.length).toBe(1); + const agent = firstAgent(plugin); expect(agent.id).toBe("karen"); expect(agent.systemPromptRole).toContain("You orchestrate."); // The Corbits Code appendix is appended at prompt-build time by @@ -68,8 +82,8 @@ describe("loadDataOnlyAgentPlugin", () => { const dir = await makePlugin({ "agents/foo.md": "---\nname: bar\ndescription: d\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { id: string; description?: string }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.id).toBe("bar"); expect(agent.description).toBe("d"); }); @@ -79,15 +93,13 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/neckbeard.md": "---\nname: neckbeard\nmode: subagent\npermission:\n read: allow\n glob: allow\n grep: allow\n bash: deny\n write: deny\n edit: deny\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - capabilities?: { mode: string; tools: string[] }; - }; - expect(agent.capabilities).toBeDefined(); + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); + const capabilities = defined(agent.capabilities, "capabilities"); // No wildcard deny, both allowed and denied lists non-empty — shorter wins. // allowed=3, denied=3 — pick exclude (smaller-or-equal rule). - expect(agent.capabilities!.mode).toBe("exclude"); - expect(agent.capabilities!.tools.sort()).toEqual(["edit_file", "run_shell", "write_file"]); + expect(capabilities.mode).toBe("exclude"); + expect(capabilities.tools.sort()).toEqual(["edit_file", "run_shell", "write_file"]); }); test("mode: primary with all-allow permission = no restriction", async () => { @@ -95,8 +107,8 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/karen.md": "---\nname: karen\nmode: primary\npermission:\n read: allow\n bash: allow\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { capabilities?: unknown }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.capabilities).toBeUndefined(); }); @@ -104,12 +116,13 @@ describe("loadDataOnlyAgentPlugin", () => { const dir = await makePlugin({ "agents/scout.md": "---\nname: scout\ntools: [Read, Grep, Glob, Bash]\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - capabilities?: { mode: string; tools: string[] }; - }; - expect(agent.capabilities!.mode).toBe("allow"); - expect(agent.capabilities!.tools.sort()).toEqual([ + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const capabilities = defined( + firstAgent(plugin).capabilities, + "capabilities", + ); + expect(capabilities.mode).toBe("allow"); + expect(capabilities.tools.sort()).toEqual([ "grep", "read_file", "run_shell", @@ -121,12 +134,13 @@ describe("loadDataOnlyAgentPlugin", () => { const dir = await makePlugin({ "agents/w.md": "---\nname: w\ndisallowedTools: [Bash, Write, Edit]\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - capabilities?: { mode: string; tools: string[] }; - }; - expect(agent.capabilities!.mode).toBe("exclude"); - expect(agent.capabilities!.tools.sort()).toEqual(["edit_file", "run_shell", "write_file"]); + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const capabilities = defined( + firstAgent(plugin).capabilities, + "capabilities", + ); + expect(capabilities.mode).toBe("exclude"); + expect(capabilities.tools.sort()).toEqual(["edit_file", "run_shell", "write_file"]); }); test("OpenCode nested permission with wildcard deny becomes allowlist", async () => { @@ -134,33 +148,35 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/r.md": '---\nname: r\npermission:\n tool:\n "*": deny\n read: allow\n grep: allow\n---\nbody\n', }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - capabilities?: { mode: string; tools: string[] }; - }; - expect(agent.capabilities!.mode).toBe("allow"); - expect(agent.capabilities!.tools.sort()).toEqual(["grep", "read_file"]); + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const capabilities = defined( + firstAgent(plugin).capabilities, + "capabilities", + ); + expect(capabilities.mode).toBe("allow"); + expect(capabilities.tools.sort()).toEqual(["grep", "read_file"]); }); test("OpenCode legacy tools: {read: true, bash: false} mixed picks shorter", async () => { const dir = await makePlugin({ "agents/m.md": "---\nname: m\ntools:\n read: true\n grep: true\n bash: false\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - capabilities?: { mode: string; tools: string[] }; - }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const capabilities = defined( + firstAgent(plugin).capabilities, + "capabilities", + ); // 1 false vs 2 true — exclude wins. - expect(agent.capabilities!.mode).toBe("exclude"); - expect(agent.capabilities!.tools).toEqual(["run_shell"]); + expect(capabilities.mode).toBe("exclude"); + expect(capabilities.tools).toEqual(["run_shell"]); }); test("bare tier frontmatter is ignored (tiers were removed)", async () => { const dir = await makePlugin({ "agents/a.md": "---\ntier: clever\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { inference?: unknown }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.inference).toBeUndefined(); }); @@ -168,8 +184,8 @@ describe("loadDataOnlyAgentPlugin", () => { const dir = await makePlugin({ "agents/a.md": "---\neffort: high\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { inference?: unknown }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.inference).toBeUndefined(); }); @@ -178,16 +194,13 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/a.md": "---\ninference:\n order:\n - { provider: anthropic, model: claude-sonnet-4, reasoningEffort: medium }\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - inference?: { - mode?: string; - order: { provider: string; model: string; reasoningEffort?: string }[]; - }; - }; - expect(agent.inference).toBeDefined(); - expect(agent.inference!.mode).toBe("prefer"); - expect(agent.inference!.order[0]).toEqual({ + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const inference = defined( + firstAgent(plugin).inference, + "inference", + ); + expect(inference.mode).toBe("prefer"); + expect(inference.order[0]).toEqual({ provider: "anthropic", model: "claude-sonnet-4", reasoningEffort: "medium", @@ -199,20 +212,21 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/a.md": "---\ninference:\n order:\n - { provider: anthropic, model: claude-sonnet-4 }\n - { provider: xai }\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - inference?: { order: { provider: string; model: string }[] }; - }; - expect(agent.inference!.order).toHaveLength(1); - expect(agent.inference!.order[0]).toEqual({ provider: "anthropic", model: "claude-sonnet-4" }); + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const inference = defined( + firstAgent(plugin).inference, + "inference", + ); + expect(inference.order).toHaveLength(1); + expect(inference.order[0]).toEqual({ provider: "anthropic", model: "claude-sonnet-4" }); }); test("native capabilities block with a non-boolean mode falls through instead of restricting", async () => { const dir = await makePlugin({ "agents/a.md": "---\ncapabilities:\n mode: sometimes\n tools: [read_file]\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { capabilities?: unknown }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.capabilities).toBeUndefined(); }); @@ -221,13 +235,13 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/a.md": "---\ncapabilities:\n mode: allow\n tools: [read_file, 42, grep]\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - capabilities?: { mode: string; tools: string[] }; - }; - expect(agent.capabilities).toBeDefined(); - expect(agent.capabilities!.mode).toBe("allow"); - expect(agent.capabilities!.tools.sort()).toEqual(["grep", "read_file"]); + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const capabilities = defined( + firstAgent(plugin).capabilities, + "capabilities", + ); + expect(capabilities.mode).toBe("allow"); + expect(capabilities.tools.sort()).toEqual(["grep", "read_file"]); }); test("model: array becomes a prefer chain", async () => { @@ -235,13 +249,16 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/a.md": "---\nmodel:\n - { provider: anthropic, model: claude-sonnet-4 }\n - { provider: xai, model: grok-4 }\n---\nbody\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { - inference?: { mode?: string; order: { provider: string; model: string }[] }; - }; - expect(agent.inference!.order.length).toBe(2); - expect(agent.inference!.order[0]!.provider).toBe("anthropic"); - expect(agent.inference!.order[1]!.provider).toBe("xai"); + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const inference = defined( + firstAgent(plugin).inference, + "inference", + ); + expect(inference.order.length).toBe(2); + expect(defined(inference.order[0], "first inference leg").provider).toBe( + "anthropic", + ); + expect(defined(inference.order[1], "second inference leg").provider).toBe("xai"); }); test("frontmatter skills list bundles skill text into the prompt", async () => { @@ -249,8 +266,8 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/a.md": "---\nskills: [style]\n---\nagent body\n", "skills/style/SKILL.md": "---\nname: style\n---\nBe clean.\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { systemPromptRole: string }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.systemPromptRole).toContain("Bundled skill: style"); expect(agent.systemPromptRole).toContain("Be clean."); expect(agent.systemPromptRole).toContain("agent body"); @@ -261,8 +278,8 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/a.md": '---\nskills: ["./skills/style"]\n---\nagent body\n', "skills/style/SKILL.md": "---\nname: style\n---\nRelative clean.\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { systemPromptRole: string }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.systemPromptRole).toContain("Bundled skill: ./skills/style"); expect(agent.systemPromptRole).toContain("Relative clean."); }); @@ -272,12 +289,14 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/a.md": "---\nskills: [/etc/passwd]\n---\nbody\n", }); const warnings: string[] = []; - const plugin = await loadDataOnlyAgentPlugin(dir, { - pluginId: "p", - onWarning: (m) => warnings.push(m), - }); - expect(plugin).not.toBeNull(); - const agent = plugin!.agentPlugin.agents[0] as { systemPromptRole: string }; + const plugin = defined( + await loadDataOnlyAgentPlugin(dir, { + pluginId: "p", + onWarning: (m) => warnings.push(m), + }), + "plugin", + ); + const agent = firstAgent(plugin); expect(agent.systemPromptRole).not.toContain("Bundled skill"); expect(warnings.some((w) => w.includes("/etc/passwd"))).toBe(true); }); @@ -289,8 +308,8 @@ describe("loadDataOnlyAgentPlugin", () => { "skills/style/SKILL.md": "Be clean.", "skills/philosophy/SKILL.md": "Be principled.", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }); - const agent = plugin!.agentPlugin.agents[0] as { systemPromptRole: string }; + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "p" }), "plugin"); + const agent = firstAgent(plugin); expect(agent.systemPromptRole).toContain("Bundled skill: style"); expect(agent.systemPromptRole).toContain("Bundled skill: philosophy"); }); @@ -315,12 +334,15 @@ describe("loadDataOnlyAgentPlugin", () => { "agents/bad.md": "this has no frontmatter at all but is valid markdown\n", }); const warnings: string[] = []; - const plugin = await loadDataOnlyAgentPlugin(dir, { - pluginId: "p", - onWarning: (m) => warnings.push(m), - }); + const plugin = defined( + await loadDataOnlyAgentPlugin(dir, { + pluginId: "p", + onWarning: (m) => warnings.push(m), + }), + "plugin", + ); // Both load — no-frontmatter is acceptable (synthesized from body alone). - expect(plugin!.agentPlugin.agents.length).toBe(2); + expect(plugin.agentPlugin.agents.length).toBe(2); expect(warnings.length).toBe(0); }); @@ -328,24 +350,21 @@ describe("loadDataOnlyAgentPlugin", () => { const dir = await makePlugin({ "agents/a.md": "body\n", }); - const plugin = await loadDataOnlyAgentPlugin(dir); - const expected = dir.split("/").pop() as string; - expect(plugin).not.toBeNull(); - expect(plugin!.manifest.id).toBe(expected); + const plugin = defined(await loadDataOnlyAgentPlugin(dir), "plugin"); + const expected = defined(dir.split("/").pop(), "plugin id"); + expect(plugin.manifest.id).toBe(expected); }); test("loads agents directly in the plugin dir (no agents/ subfolder)", async () => { const dir = await makePlugin({ "alpha.md": "---\nid: alpha\ndescription: direct\n---\nDirect agent body", }); - const plugin = await loadDataOnlyAgentPlugin(dir, { pluginId: "flat" }); - expect(plugin).not.toBeNull(); - expect(plugin!.manifest.id).toBe("flat"); - expect(plugin!.agentPlugin.agents.length).toBe(1); - expect((plugin!.agentPlugin.agents[0] as AgentProfile).id).toBe("alpha"); - expect((plugin!.agentPlugin.agents[0] as AgentProfile).systemPromptRole).toContain( - "Direct agent body", - ); + const plugin = defined(await loadDataOnlyAgentPlugin(dir, { pluginId: "flat" }), "plugin"); + expect(plugin.manifest.id).toBe("flat"); + expect(plugin.agentPlugin.agents.length).toBe(1); + const agent = firstAgent(plugin); + expect(agent.id).toBe("alpha"); + expect(agent.systemPromptRole).toContain("Direct agent body"); }); test("supports pointing at agents/ subdir directly; id comes from parent; skills resolve from sibling", async () => { @@ -354,13 +373,12 @@ describe("loadDataOnlyAgentPlugin", () => { "skills/style/SKILL.md": "Style rules: be concise.", }); const agentsSub = join(dir, "agents"); - const plugin = await loadDataOnlyAgentPlugin(agentsSub); - expect(plugin).not.toBeNull(); + const plugin = defined(await loadDataOnlyAgentPlugin(agentsSub), "plugin"); // id derives from parent dir name, not "agents" - const expectedId = dir.split("/").pop() as string; - expect(plugin!.manifest.id).toBe(expectedId); - expect(plugin!.agentPlugin.agents.length).toBe(1); - const prof = plugin!.agentPlugin.agents[0] as AgentProfile; + const expectedId = defined(dir.split("/").pop(), "plugin id"); + expect(plugin.manifest.id).toBe(expectedId); + expect(plugin.agentPlugin.agents.length).toBe(1); + const prof = firstAgent(plugin); expect(prof.id).toBe("beta"); expect(prof.systemPromptRole).toContain("Bundled skill: style"); expect(prof.systemPromptRole).toContain("Style rules: be concise."); diff --git a/tests/unit/data-only-commands.test.ts b/tests/unit/data-only-commands.test.ts index 7f1985a1d..6af7120ec 100644 --- a/tests/unit/data-only-commands.test.ts +++ b/tests/unit/data-only-commands.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import type { CommandContext } from "../../src/tui/commands/registry.js"; import { loadDataOnlyCommands } from "../../src/plugins/data-only-commands.js"; import { loadDataOnlyPlugin } from "../../src/plugins/data-only.js"; +import { defined } from "../helpers/defined.js"; let root: string; @@ -18,7 +19,7 @@ async function makePlugin(layout: Record): Promise { return dir; } -const ctx: CommandContext = { signalClear: () => {} }; +const ctx: CommandContext = { signalClear: () => undefined }; beforeEach(async () => { root = await mkdtemp(); @@ -49,12 +50,13 @@ describe("loadDataOnlyCommands", () => { const dir = await makePlugin({ "commands/greet.md": "---\ndescription: Greet someone\n---\nHello $ARGUMENTS!", }); - const plugin = await loadDataOnlyCommands(dir); - expect(plugin).not.toBeNull(); - const cmd = plugin!.commandPlugin.commands.find((c) => c.name === "greet"); - expect(cmd).toBeDefined(); - expect(cmd!.description).toBe("Greet someone"); - const res = cmd!.handler("world", ctx); + const plugin = defined(await loadDataOnlyCommands(dir), "plugin"); + const cmd = defined( + plugin.commandPlugin.commands.find((c) => c.name === "greet"), + "greet command", + ); + expect(cmd.description).toBe("Greet someone"); + const res = cmd.handler("world", ctx); expect(res).toEqual({ type: "send", text: "Hello world!" }); }); @@ -63,9 +65,12 @@ describe("loadDataOnlyCommands", () => { "commands/greet.md": "---\ndescription: Greet someone\nargument-hint: \n---\nHello $ARGUMENTS!", }); - const cmd = (await loadDataOnlyCommands(dir))!.commandPlugin.commands.find( - (c) => c.name === "greet", - )!; + const cmd = defined( + defined(await loadDataOnlyCommands(dir), "plugin").commandPlugin.commands.find( + (c) => c.name === "greet", + ), + "greet command", + ); expect(cmd.argumentHint).toBe(""); }); @@ -73,13 +78,19 @@ describe("loadDataOnlyCommands", () => { const dir = await makePlugin({ "commands/plain.md": "Summarize the working tree.\nMore detail.", }); - const cmd = (await loadDataOnlyCommands(dir))!.commandPlugin.commands[0]!; + const cmd = defined( + defined(await loadDataOnlyCommands(dir), "plugin").commandPlugin.commands[0], + "command", + ); expect(cmd.description).toBe("Summarize the working tree."); }); test("drops $ARGUMENTS when the command is invoked with no args", async () => { const dir = await makePlugin({ "commands/echo.md": "Body [$ARGUMENTS] end" }); - const cmd = (await loadDataOnlyCommands(dir))!.commandPlugin.commands[0]!; + const cmd = defined( + defined(await loadDataOnlyCommands(dir), "plugin").commandPlugin.commands[0], + "command", + ); expect(cmd.handler("", ctx)).toEqual({ type: "send", text: "Body [] end" }); }); @@ -88,16 +99,18 @@ describe("loadDataOnlyCommands", () => { "commands/repo/init.md": "---\ndescription: init a repo\n---\nInit $ARGUMENTS", "commands/repo/scan.md": "---\ndescription: scan a repo\n---\nScan it", }); - const cmd = (await loadDataOnlyCommands(dir))!.commandPlugin.commands.find( - (c) => c.name === "repo", + const cmd = defined( + defined(await loadDataOnlyCommands(dir), "plugin").commandPlugin.commands.find( + (c) => c.name === "repo", + ), + "repo command", ); - expect(cmd).toBeDefined(); - expect(cmd!.subcommands?.map((s) => s.name).sort()).toEqual(["init", "scan"]); + expect(cmd.subcommands?.map((s) => s.name).sort()).toEqual(["init", "scan"]); - const ok = cmd!.handler("init acme", ctx); + const ok = cmd.handler("init acme", ctx); expect(ok).toEqual({ type: "send", text: "Init acme" }); - const missing = cmd!.handler("nope", ctx); + const missing = cmd.handler("nope", ctx); expect(missing).toEqual({ type: "message", text: 'Unknown repo subcommand "nope". Available: init, scan', @@ -106,21 +119,19 @@ describe("loadDataOnlyCommands", () => { test("accepts the OpenCode command/ (singular) root", async () => { const dir = await makePlugin({ "command/greet.md": "Hi $ARGUMENTS" }); - const plugin = await loadDataOnlyCommands(dir); - expect(plugin).not.toBeNull(); - expect(plugin!.commandPlugin.commands[0]!.name).toBe("greet"); + const plugin = defined(await loadDataOnlyCommands(dir), "plugin"); + expect(defined(plugin.commandPlugin.commands[0], "command").name).toBe("greet"); }); }); describe("loadDataOnlyPlugin command routing", () => { test("a commands-only directory infers kind command", async () => { const dir = await makePlugin({ "commands/greet.md": "Hi $ARGUMENTS" }); - const plugin = await loadDataOnlyPlugin(dir); - expect(plugin).not.toBeNull(); - expect(plugin!.manifest.kind).toBe("command"); - expect(plugin!.manifest.id).toBe(dir.split("/").pop()!); - expect(plugin!.commandPlugin).toBeDefined(); - expect(plugin!.agentPlugin).toBeUndefined(); + const plugin = defined(await loadDataOnlyPlugin(dir), "plugin"); + expect(plugin.manifest.kind).toBe("command"); + expect(plugin.manifest.id).toBe(defined(dir.split("/").pop(), "plugin id")); + expect(plugin.commandPlugin).toBeDefined(); + expect(plugin.agentPlugin).toBeUndefined(); }); test("an explicit manifest.json is authoritative", async () => { @@ -133,8 +144,8 @@ describe("loadDataOnlyPlugin command routing", () => { }), "commands/greet.md": "Hi", }); - const plugin = await loadDataOnlyPlugin(dir); - expect(plugin!.manifest).toEqual({ + const plugin = defined(await loadDataOnlyPlugin(dir), "plugin"); + expect(plugin.manifest).toEqual({ id: "my-cmds", name: "My Commands", kind: "command", @@ -154,11 +165,11 @@ describe("loadDataOnlyPlugin command routing", () => { "agents/karen.md": "You orchestrate.", "commands/greet.md": "Hi $ARGUMENTS", }); - const plugin = await loadDataOnlyPlugin(dir); - expect(plugin!.manifest.kind).toBe("agent"); + const plugin = defined(await loadDataOnlyPlugin(dir), "plugin"); + expect(plugin.manifest.kind).toBe("agent"); // Both exports are attached; commands wire as an added surface via the // agent-kind allowance in isEnabledCommandPlugin. - expect(plugin!.agentPlugin).toBeDefined(); - expect(plugin!.commandPlugin).toBeDefined(); + expect(plugin.agentPlugin).toBeDefined(); + expect(plugin.commandPlugin).toBeDefined(); }); }); diff --git a/tests/unit/director.test.ts b/tests/unit/director.test.ts index eae1acb6c..e8735e44a 100644 --- a/tests/unit/director.test.ts +++ b/tests/unit/director.test.ts @@ -138,7 +138,7 @@ const manyTurnsState: ReactorState = { function makeChatDirectorWithContinuation(onContinue: () => void) { return createChatDirector("sys", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, requestContinuation: onContinue, }); } @@ -231,7 +231,7 @@ async function runToolOnlyStreak( test("a grok provider no longer pauses a 10-turn productive tool-only streak", async () => { const grokDirector = createChatDirector("sys", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: { providerName: "xai", model: "grok-4" }, }); const grokActions = await runToolOnlyStreak(grokDirector, 10); @@ -240,7 +240,7 @@ test("a grok provider no longer pauses a 10-turn productive tool-only streak", a ); const defaultDirector = createChatDirector("sys", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, provider: { providerName: "openai", model: "gpt-4" }, }); const defaultActions = await runToolOnlyStreak(defaultDirector, 10); diff --git a/tests/unit/example-agent-plugin.test.ts b/tests/unit/example-agent-plugin.test.ts index 145a0137d..4300dd1a4 100644 --- a/tests/unit/example-agent-plugin.test.ts +++ b/tests/unit/example-agent-plugin.test.ts @@ -3,16 +3,16 @@ import { join } from "node:path"; import { loadPluginEntry } from "../../src/plugins/loader.js"; import { resolveAgentPluginProfiles } from "../../src/plugins/agent-plugins.js"; +import { defined } from "../helpers/defined.js"; const pluginRoot = join(import.meta.dirname, "../fixtures/plugins/example-agent"); test("example-agent plugin loads scout profile when enabled", async () => { - const mod = await loadPluginEntry(pluginRoot); - expect(mod).not.toBeNull(); - expect(mod!.manifest?.id).toBe("example-agent"); - expect(mod!.manifest?.kind).toBe("agent"); + const mod = defined(await loadPluginEntry(pluginRoot), "plugin module"); + expect(mod.manifest?.id).toBe("example-agent"); + expect(mod.manifest?.kind).toBe("agent"); - const profiles = await resolveAgentPluginProfiles([mod!], { + const profiles = await resolveAgentPluginProfiles([mod], { "example-agent": { enabled: true }, }); expect(profiles.map((p) => p.id)).toEqual(["scout"]); diff --git a/tests/unit/exec/runner.test.ts b/tests/unit/exec/runner.test.ts index bec0b8516..d3c0098ad 100644 --- a/tests/unit/exec/runner.test.ts +++ b/tests/unit/exec/runner.test.ts @@ -19,6 +19,7 @@ import { getActiveDisposeHost } from "../../../src/session/active-host.js"; import { loadState, type RunState } from "../../../src/session/state.js"; import type { AgentToolset } from "../../../src/agent/tools.js"; import { createSubAgentSessionStore } from "../../../src/subagent/session-store.js"; +import { defined } from "../../helpers/defined.js"; import { withMockedModuleDuring } from "../../helpers/mock-module.js"; function bareConfig(task: string): Config { @@ -427,7 +428,7 @@ describe("disposeExecRuntime", () => { test("reaps the toolset before waiting on a hung agent close", async () => { const calls: string[] = []; - let releaseClose!: () => void; + let releaseClose: (() => void) | undefined; const closeGate = new Promise((resolve) => { releaseClose = resolve; }); @@ -447,7 +448,7 @@ describe("disposeExecRuntime", () => { }); await new Promise((resolve) => setTimeout(resolve, 20)); expect(calls).toEqual(["toolset"]); - releaseClose(); + defined<() => void>(releaseClose, "releaseClose")(); await pending; expect(calls).toEqual(["toolset", "agent"]); }); @@ -472,7 +473,7 @@ describe("disposeExecRuntime", () => { agent: { close: () => { closeStarted = true; - return new Promise(() => {}); + return new Promise(() => undefined); }, }, toolset: { diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index d600d6a0f..59764150f 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -9,6 +9,7 @@ import { schedulePricingMetadataRefresh, } from "../../src/cost/pricing-metadata.js"; import { cliCaughtExit, mainWithRunners } from "../../src/index.js"; +import { defined } from "../helpers/defined.js"; const envVars = { // Unit tests must never export telemetry or write an installationId into @@ -117,7 +118,7 @@ test("main launches exec when configured with exec subcommand", async () => { expect(code).toBe(0); expect(runExec).toHaveBeenCalled(); expect(runTUI).not.toHaveBeenCalled(); - const cfg = runExec.mock.calls[0]![0]; + const cfg = defined(runExec.mock.calls[0], "runExec call")[0]; expect(cfg.command).toBe("exec"); expect(cfg.task).toBe("say hello"); }); @@ -135,7 +136,7 @@ test("main launches exec for run alias", async () => { }); expect(code).toBe(0); expect(runExec).toHaveBeenCalled(); - const cfg = runExec.mock.calls[0]![0]; + const cfg = defined(runExec.mock.calls[0], "runExec call")[0]; expect(cfg.command).toBe("exec"); expect(cfg.task).toBe("do the thing"); }); diff --git a/tests/unit/mcp.test.ts b/tests/unit/mcp.test.ts index 8e6e9a400..e18f342d6 100644 --- a/tests/unit/mcp.test.ts +++ b/tests/unit/mcp.test.ts @@ -10,6 +10,7 @@ import { createOAuthProvider } from "../../src/mcp/oauth-provider.js"; import { createDynamicToolRunner } from "../../src/tui/dynamic-tool-runner.js"; import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"; import type { MCPClient } from "../../src/mcp/client.js"; +import { defined } from "../helpers/defined.js"; import { DuplicateToolError, type AgentTool } from "@intx/agent"; describe("isLocalSettings with mcpServers", () => { @@ -119,7 +120,9 @@ function makeFakeClient(serverName: string, toolNames: string[]): MCPClient { async call() { return "result"; }, - async close() {}, + async close() { + return undefined; + }, }; } @@ -149,7 +152,7 @@ describe("mcpClientToAgentTools (production gated path)", () => { reactorGated: false, }); const tools = mcpClientToAgentTools(client, gate); - expect(tools[0]!.definition.description).toBe("[github] search_repos tool"); + expect(defined(tools[0], "mcp tool").definition.description).toBe("[github] search_repos tool"); }); test("tool handler returns call result", async () => { @@ -164,7 +167,9 @@ describe("mcpClientToAgentTools (production gated path)", () => { capturedArgs = args; return "done"; }, - async close() {}, + async close() { + return undefined; + }, }; const gate = createPermissionGate({ @@ -173,7 +178,7 @@ describe("mcpClientToAgentTools (production gated path)", () => { skipPermissions: true, reactorGated: false, }); - const tool = mcpClientToAgentTools(client, gate)[0]!; + const tool = defined(mcpClientToAgentTools(client, gate)[0], "mcp tool"); const result = await tool.handler( { id: "c1", name: "mcp__myserver__do_thing", arguments: { x: 1 } }, new AbortController().signal, @@ -193,7 +198,9 @@ describe("mcpClientToAgentTools (production gated path)", () => { async call() { throw new Error("server error"); }, - async close() {}, + async close() { + return undefined; + }, }; const gate = createPermissionGate({ @@ -202,7 +209,7 @@ describe("mcpClientToAgentTools (production gated path)", () => { skipPermissions: true, reactorGated: false, }); - const tool = mcpClientToAgentTools(client, gate)[0]!; + const tool = defined(mcpClientToAgentTools(client, gate)[0], "mcp tool"); const result = await tool.handler( { id: "c1", name: "mcp__srv__fail", arguments: {} }, new AbortController().signal, @@ -227,7 +234,7 @@ describe("mcpClientToAgentTools (production gated path)", () => { }); const client = makeFakeClient("acme", ["save_issue"]); gate.registerMcpClient(client); - const tool = mcpClientToAgentTools(client, gate)[0]!; + const tool = defined(mcpClientToAgentTools(client, gate)[0], "mcp tool"); const result = await tool.handler( { id: "c1", name: "mcp__acme__save_issue", arguments: { id: "X-1" } }, new AbortController().signal, @@ -374,7 +381,7 @@ describe("OAuth provider", () => { serverName: "acme", serverURL: acmeAuthIdentity.serverURL, redirectUrl: "http://127.0.0.1:0/cb", - onAuthURL: () => {}, + onAuthURL: () => undefined, home, }); const first = await provider.state?.(); @@ -392,7 +399,7 @@ describe("OAuth provider", () => { serverName: "acme", serverURL: acmeAuthIdentity.serverURL, redirectUrl: "http://127.0.0.1:0/cb", - onAuthURL: () => {}, + onAuthURL: () => undefined, home, }); await first.saveTokens({ access_token: "abc", token_type: "Bearer" }); @@ -400,7 +407,7 @@ describe("OAuth provider", () => { serverName: "acme", serverURL: acmeAuthIdentity.serverURL, redirectUrl: "http://127.0.0.1:0/cb", - onAuthURL: () => {}, + onAuthURL: () => undefined, home, }); expect(second.tokens()).toEqual({ access_token: "abc", token_type: "Bearer" }); @@ -416,7 +423,7 @@ describe("OAuth provider", () => { serverName: "acme", serverURL: acmeAuthIdentity.serverURL, redirectUrl: "http://127.0.0.1:0/cb", - onAuthURL: () => {}, + onAuthURL: () => undefined, home, }); await provider.saveTokens({ diff --git a/tests/unit/path-plugin-trust.test.ts b/tests/unit/path-plugin-trust.test.ts index 542e40caf..d9c823da1 100644 --- a/tests/unit/path-plugin-trust.test.ts +++ b/tests/unit/path-plugin-trust.test.ts @@ -9,6 +9,7 @@ import { loadPluginsFromPaths, type ExpandPluginPathSkip, } from "../../src/plugins/loader.js"; +import { defined } from "../helpers/defined.js"; import { isPathPluginTrusted, loadPathTrust, @@ -39,7 +40,7 @@ async function writeCommandPlugin(dir: string, id: string, marker?: string): Pro await writeFile( join(dir, "index.ts"), `${sideEffect}export const manifest = { id: ${JSON.stringify(id)}, name: ${JSON.stringify(id)}, kind: "command" }; -export const commandPlugin = { commands: [{ name: "ping", description: "ping", run: async () => {} }] }; +export const commandPlugin = { commands: [{ name: "ping", description: "ping", run: async () => undefined }] }; `, "utf8", ); @@ -253,8 +254,9 @@ describe("path plugin trust across working directories", () => { isPluginTrusted: (p) => isPathPluginTrusted(pathTrust, p), }); expect(mods.map((m) => m.manifest?.id)).toEqual(["gamma"]); - expect(mods[0]!.metadataOnly).toBeUndefined(); - expect(mods[0]!.pluginPath).toBe(sibling); + const mod = defined(mods[0], "plugin module"); + expect(mod.metadataOnly).toBeUndefined(); + expect(mod.pluginPath).toBe(sibling); } finally { await rm(base, { recursive: true, force: true }); } diff --git a/tests/unit/plugin-loader-path.test.ts b/tests/unit/plugin-loader-path.test.ts index 3119e6f44..29717dbbd 100644 --- a/tests/unit/plugin-loader-path.test.ts +++ b/tests/unit/plugin-loader-path.test.ts @@ -1,12 +1,12 @@ import { test, expect } from "bun:test"; import { loadPluginEntry, loadPluginsFromPaths } from "../../src/plugins/loader.js"; +import { defined } from "../helpers/defined.js"; test("loadPluginEntry loads a plugin directory by path and reads its manifest", async () => { - const mod = await loadPluginEntry("tests/fixtures/plugins/exa"); - expect(mod).not.toBeNull(); - expect(mod!.manifest?.id).toBe("exa"); - expect(mod!.manifest?.kind).toBe("web"); - expect(typeof mod!.createWebProvider).toBe("function"); + const mod = defined(await loadPluginEntry("tests/fixtures/plugins/exa"), "plugin module"); + expect(mod.manifest?.id).toBe("exa"); + expect(mod.manifest?.kind).toBe("web"); + expect(typeof mod.createWebProvider).toBe("function"); }); test("loadPluginEntry returns null for a non-existent path", async () => { diff --git a/tests/unit/plugin-marketplace.test.ts b/tests/unit/plugin-marketplace.test.ts index d43661ae9..e077e3d17 100644 --- a/tests/unit/plugin-marketplace.test.ts +++ b/tests/unit/plugin-marketplace.test.ts @@ -8,6 +8,7 @@ import { loadPluginsFromPaths, type ExpandPluginPathSkip, } from "../../src/plugins/loader.js"; +import { defined } from "../helpers/defined.js"; test("a marketplace path expands to its declared member plugins", async () => { const mods = await loadPluginsFromPaths(["tests/fixtures/marketplace"], process.cwd()); @@ -32,7 +33,7 @@ test("a normal plugin directory is not expanded (no marketplace.json, no plugins process.cwd(), ); expect(mods.length).toBe(1); - expect(mods[0]!.manifest?.id).toBe("example-commands"); + expect(defined(mods[0], "plugin module").manifest?.id).toBe("example-commands"); }); test("mixed catalog: relative sibling loads; absolute and escape are skipped", async () => { diff --git a/tests/unit/project-trust-plugins.test.ts b/tests/unit/project-trust-plugins.test.ts index 69d21b4be..0158777b7 100644 --- a/tests/unit/project-trust-plugins.test.ts +++ b/tests/unit/project-trust-plugins.test.ts @@ -55,7 +55,7 @@ export const commandPlugin = { commands: [] }; await writeFile( join(pluginDir, "index.ts"), `export const manifest = { id: "ok-plugin", name: "OK", kind: "command" }; -export const commandPlugin = { commands: [{ name: "ping", description: "ping", run: async () => {} }] }; +export const commandPlugin = { commands: [{ name: "ping", description: "ping", run: async () => undefined }] }; `, "utf8", ); diff --git a/tests/unit/ripgrep-plugin.test.ts b/tests/unit/ripgrep-plugin.test.ts index 295a947ab..7ec95e672 100644 --- a/tests/unit/ripgrep-plugin.test.ts +++ b/tests/unit/ripgrep-plugin.test.ts @@ -10,6 +10,7 @@ import { ripgrepPlugin } from "../../src/plugins/ripgrep-plugin.js"; import { MAX_RESULT_CHARS } from "../../src/plugins/result-truncation-plugin.js"; import { buildCorePosixToolPlugins } from "../../src/agent/posix-tool-plugins.js"; import { createPermissionGate } from "../../src/permission/gate.js"; +import { defined } from "../helpers/defined.js"; import type { RgChild, SpawnRg } from "../../src/plugins/rg-run.js"; // Repo root derived from this file, not process.cwd(): these cases search real @@ -26,7 +27,9 @@ function run( limits: { timeoutMs?: number; maxOutputBytes?: number } = {}, spawnChild?: SpawnRg, ): Promise { - const handler = ripgrepPlugin(cwd, limits, spawnChild).middleware!(fallback); + const handler = defined(ripgrepPlugin(cwd, limits, spawnChild).middleware, "ripgrep middleware")( + fallback, + ); return handler(call, new AbortController().signal); } diff --git a/tests/unit/skill-commands.test.ts b/tests/unit/skill-commands.test.ts index 3221f61c2..89eade0fc 100644 --- a/tests/unit/skill-commands.test.ts +++ b/tests/unit/skill-commands.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import type { CommandContext } from "../../src/tui/commands/registry.js"; import { loadSkillCommands } from "../../src/plugins/skill-commands.js"; import { loadDataOnlyPlugin } from "../../src/plugins/data-only.js"; +import { defined } from "../helpers/defined.js"; let root: string; @@ -18,7 +19,7 @@ async function makePlugin(layout: Record): Promise { return dir; } -const ctx: CommandContext = { signalClear: () => {} }; +const ctx: CommandContext = { signalClear: () => undefined }; beforeEach(async () => { root = await mkdtemp(); @@ -47,11 +48,14 @@ describe("loadSkillCommands", () => { "skills/linear-create/SKILL.md": "---\nname: linear-create\ndescription: Create Linear issues\n---\nCreate the artifacts.", }); - const cmds = await loadSkillCommands(dir); - expect(cmds!.map((c) => c.name).sort()).toEqual(["linear-create", "linear-issue-workflow"]); + const cmds = defined(await loadSkillCommands(dir), "skill commands"); + expect(cmds.map((c) => c.name).sort()).toEqual(["linear-create", "linear-issue-workflow"]); // Tagged skill, no $ARGUMENTS in body -> args append. - const workflow = cmds!.find((c) => c.name === "linear-issue-workflow")!; + const workflow = defined( + cmds.find((c) => c.name === "linear-issue-workflow"), + "linear-issue-workflow command", + ); expect(workflow.description).toBe("Implement a Linear issue"); expect(workflow.argumentHint).toBe(""); expect(workflow.handler("ABC-123", ctx)).toEqual({ @@ -60,7 +64,7 @@ describe("loadSkillCommands", () => { }); // Untagged skill still becomes a command. - expect(cmds!.find((c) => c.name === "linear-create")).toBeDefined(); + expect(cmds.find((c) => c.name === "linear-create")).toBeDefined(); }); test("omits a skill with user-invocable: false; sibling without the flag is still present", async () => { @@ -70,8 +74,8 @@ describe("loadSkillCommands", () => { "skills/linear-create/SKILL.md": "---\nname: linear-create\ndescription: Create Linear issues\n---\nCreate the artifacts.", }); - const cmds = await loadSkillCommands(dir); - expect(cmds!.map((c) => c.name).sort()).toEqual(["linear-create"]); + const cmds = defined(await loadSkillCommands(dir), "skill commands"); + expect(cmds.map((c) => c.name).sort()).toEqual(["linear-create"]); }); test("copies argument-hint from skill frontmatter", async () => { @@ -79,7 +83,10 @@ describe("loadSkillCommands", () => { "skills/linear-create/SKILL.md": '---\nname: linear-create\ndescription: Create Linear issues\nargument-hint: "[description] [--from-doc]"\n---\nCreate the artifacts.', }); - const cmd = (await loadSkillCommands(dir))!.find((c) => c.name === "linear-create")!; + const cmd = defined( + defined(await loadSkillCommands(dir), "skill commands").find((c) => c.name === "linear-create"), + "linear-create command", + ); expect(cmd.argumentHint).toBe("[description] [--from-doc]"); }); @@ -88,7 +95,10 @@ describe("loadSkillCommands", () => { "skills/hiring/SKILL.md": "---\nname: hiring\ndescription: Hiring\n---\nRun the loop on $ARGUMENTS.", }); - const cmd = (await loadSkillCommands(dir))!.find((c) => c.name === "hiring")!; + const cmd = defined( + defined(await loadSkillCommands(dir), "skill commands").find((c) => c.name === "hiring"), + "hiring command", + ); expect(cmd.handler("analyze", ctx)).toEqual({ type: "send", text: "Run the loop on analyze." }); expect(cmd.handler("", ctx)).toEqual({ type: "send", text: "Run the loop on ." }); }); @@ -97,8 +107,8 @@ describe("loadSkillCommands", () => { const dir = await makePlugin({ "skills/custom-name/SKILL.md": "---\ndescription: d\n---\nBody.", }); - const cmds = await loadSkillCommands(dir); - expect(cmds!.map((c) => c.name)).toEqual(["custom-name"]); + const cmds = defined(await loadSkillCommands(dir), "skill commands"); + expect(cmds.map((c) => c.name)).toEqual(["custom-name"]); }); }); @@ -115,16 +125,16 @@ describe("loadDataOnlyPlugin — Claude marketplace adapter", () => { "skills/linear-issue-workflow/SKILL.md": "---\nname: linear-issue-workflow\ndisable-model-invocation: true\n---\nDo it.", }); - const plugin = await loadDataOnlyPlugin(dir); - expect(plugin!.manifest).toEqual({ + const plugin = defined(await loadDataOnlyPlugin(dir), "plugin"); + expect(plugin.manifest).toEqual({ id: "gaas", name: "gaas", kind: "agent", description: "Dev skills and agents", }); // Agents wire (kind agent) AND every skill wires as a command. - expect(plugin!.agentPlugin).toBeDefined(); - expect(plugin!.commandPlugin?.commands.map((c) => c.name).sort()).toEqual([ + expect(plugin.agentPlugin).toBeDefined(); + expect(plugin.commandPlugin?.commands.map((c) => c.name).sort()).toEqual([ "linear-create", "linear-issue-workflow", ]); @@ -136,10 +146,10 @@ describe("loadDataOnlyPlugin — Claude marketplace adapter", () => { ".claude-plugin/plugin.json": JSON.stringify({ name: "claude-name", description: "ignored" }), "agents/karen.md": "You orchestrate.", }); - const plugin = await loadDataOnlyPlugin(dir); - expect(plugin!.manifest.id).toBe("native"); - expect(plugin!.manifest.name).toBe("Native"); - expect(plugin!.manifest.description).toBeUndefined(); + const plugin = defined(await loadDataOnlyPlugin(dir), "plugin"); + expect(plugin.manifest.id).toBe("native"); + expect(plugin.manifest.name).toBe("Native"); + expect(plugin.manifest.description).toBeUndefined(); }); test("a skills-only plugin infers kind command (every skill is a command)", async () => { @@ -147,12 +157,12 @@ describe("loadDataOnlyPlugin — Claude marketplace adapter", () => { "skills/hiring/SKILL.md": "---\nname: hiring\n---\nHire.", "skills/brand-identity/SKILL.md": "---\nname: brand-identity\n---\nBrand.", }); - const plugin = await loadDataOnlyPlugin(dir); - expect(plugin!.manifest.kind).toBe("command"); - expect(plugin!.commandPlugin?.commands.map((c) => c.name).sort()).toEqual([ + const plugin = defined(await loadDataOnlyPlugin(dir), "plugin"); + expect(plugin.manifest.kind).toBe("command"); + expect(plugin.commandPlugin?.commands.map((c) => c.name).sort()).toEqual([ "brand-identity", "hiring", ]); - expect(plugin!.agentPlugin).toBeUndefined(); + expect(plugin.agentPlugin).toBeUndefined(); }); }); diff --git a/tests/unit/skills.test.ts b/tests/unit/skills.test.ts index b8884e89f..6373b2f5c 100644 --- a/tests/unit/skills.test.ts +++ b/tests/unit/skills.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { test, expect, describe, beforeEach, afterEach } from "bun:test"; import { discoverSkills, resolveSkillBody } from "../../src/extensions/skills.js"; +import { defined } from "../helpers/defined.js"; const fixtureCwd = join(import.meta.dirname, "../fixtures/skill-workspace"); const exampleAgentPlugin = join(import.meta.dirname, "../fixtures/plugins/example-agent"); @@ -14,7 +15,7 @@ describe("skill discovery", () => { const skills = await discoverSkills(fixtureCwd, pluginDirs); const scribe = skills.find((s) => s.name === "scribe"); expect(scribe).toBeDefined(); - expect(scribe!.description.length).toBeGreaterThan(0); + expect(defined(scribe, "scribe skill").description.length).toBeGreaterThan(0); }); test("dedupes by name", async () => { @@ -78,7 +79,7 @@ describe("skill resolution", () => { test("resolves plugin skill body with frontmatter stripped", async () => { const body = await resolveSkillBody(fixtureCwd, "scribe", pluginDirs); expect(body).toBeDefined(); - expect(body!.startsWith("---")).toBe(false); + expect(defined(body, "skill body").startsWith("---")).toBe(false); expect(body).toContain("Scribe"); }); @@ -103,7 +104,7 @@ describe("skill resolution", () => { const body = await resolveSkillBody(plugin, "git-worktrees", [plugin]); expect(body).toBeDefined(); expect(body).toContain("Create worktree recipe."); - expect(body!.startsWith("---")).toBe(false); + expect(defined(body, "skill body").startsWith("---")).toBe(false); } finally { await rm(plugin, { recursive: true, force: true }); } @@ -133,7 +134,7 @@ describe("path-like skill refs", () => { }); expect(body).toBeDefined(); expect(body).toContain("Be clean and direct."); - expect(body!.startsWith("---")).toBe(false); + expect(defined(body, "skill body").startsWith("---")).toBe(false); }); test("resolves relative SKILL.md file ref under pluginRoot", async () => { diff --git a/tests/unit/subagent-session-store.test.ts b/tests/unit/subagent-session-store.test.ts index 7f0bf5922..c5861ce5c 100644 --- a/tests/unit/subagent-session-store.test.ts +++ b/tests/unit/subagent-session-store.test.ts @@ -283,7 +283,7 @@ describe("createSubAgentSessionStore", () => { const store = createSubAgentSessionStore({ createId: () => "s-int" }); store.start({ description: "loop", agentId: "worker", brief: "b", retained: true }); store.markRunning("s-int"); - store.registerInterrupt("s-int", () => {}); + store.registerInterrupt("s-int", () => undefined); store.registerFollowup("s-int", async () => "next"); expect(store.interruptOne("s-int").ok).toBe(true); const session = store.get("s-int"); diff --git a/tests/unit/telemetry-product-events.test.ts b/tests/unit/telemetry-product-events.test.ts index 6348432a5..a695f37c8 100644 --- a/tests/unit/telemetry-product-events.test.ts +++ b/tests/unit/telemetry-product-events.test.ts @@ -9,6 +9,7 @@ import { afterEach, expect, test } from "bun:test"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { defined } from "../helpers/defined.js"; import { createUseSkillTool } from "../../src/agent/use-skill.js"; import type { Settings } from "../../src/config/settings.js"; @@ -77,7 +78,7 @@ const tempDirs: string[] = []; afterEach(async () => { resetFeedbackStateForTests(); while (tempDirs.length > 0) { - await rm(tempDirs.pop()!, { recursive: true, force: true }); + await rm(defined(tempDirs.pop(), "temp dir"), { recursive: true, force: true }); } }); diff --git a/tests/unit/telemetry-singleton.test.ts b/tests/unit/telemetry-singleton.test.ts index 61477a263..94e68d7b8 100644 --- a/tests/unit/telemetry-singleton.test.ts +++ b/tests/unit/telemetry-singleton.test.ts @@ -18,8 +18,8 @@ test("setTelemetry replaces the process-wide instance", () => { captured = event; }, captureIntentional: () => false, - flush: async () => {}, - discard: () => {}, + flush: async () => undefined, + discard: () => undefined, }); getTelemetry().capture("session_end"); expect(captured).toBe("session_end"); diff --git a/tests/unit/telemetry-toggle.test.ts b/tests/unit/telemetry-toggle.test.ts index 82e98975b..483ebfcbd 100644 --- a/tests/unit/telemetry-toggle.test.ts +++ b/tests/unit/telemetry-toggle.test.ts @@ -6,6 +6,7 @@ import { import { createTelemetry, getSessionId } from "../../src/telemetry/index.js"; import type { Settings } from "../../src/config/settings.js"; import type { Telemetry } from "../../src/telemetry/index.js"; +import { defined } from "../helpers/defined.js"; function fakeDeps(overrides: Partial = {}): { deps: TelemetryToggleDeps; @@ -37,7 +38,7 @@ function fakeDeps(overrides: Partial = {}): { providers: {}, telemetry: { enabled: true, installationId: "id" }, }), - saveGlobalSettings: async () => {}, + saveGlobalSettings: async () => undefined, // env is pinned to {} (matching telemetry.test.ts) so a developer's real // DO_NOT_TRACK / CORBITS_TELEMETRY never bleeds into these tests. createTelemetry: (opts) => @@ -199,10 +200,10 @@ test("toggle on while env-killed writes nothing and swaps no instance", async () const initial: Telemetry = { enabled: false, installationId: "", - capture: () => {}, + capture: () => undefined, captureIntentional: () => false, - flush: async () => {}, - discard: () => {}, + flush: async () => undefined, + discard: () => undefined, }; let setInstance: Telemetry | undefined; const { deps } = fakeDeps({ @@ -324,9 +325,15 @@ test("session_id on captured payloads stays constant across an enable/disable/en await getInstance().flush(); expect(capturedBodies.length).toBe(2); - const sessionId = capturedBodies[0]!.batch[0]!.properties.session_id; + const sessionId = defined( + defined(capturedBodies[0], "first body").batch[0], + "first event", + ).properties.session_id; expect(typeof sessionId).toBe("string"); expect((sessionId as string).length).toBeGreaterThan(0); - expect(capturedBodies[1]!.batch[0]!.properties.session_id).toBe(sessionId); + expect( + defined(defined(capturedBodies[1], "second body").batch[0], "second event").properties + .session_id, + ).toBe(sessionId); expect(sessionId).toBe(getSessionId()); }); diff --git a/tests/unit/telemetry.test.ts b/tests/unit/telemetry.test.ts index f038a881d..51d19bf7a 100644 --- a/tests/unit/telemetry.test.ts +++ b/tests/unit/telemetry.test.ts @@ -10,6 +10,7 @@ import { } from "../../src/telemetry/index.js"; import { ensureTelemetrySettings } from "../../src/config/settings.js"; import type { Settings } from "../../src/config/settings.js"; +import { defined } from "../helpers/defined.js"; function settingsWith(installationId?: string, enabled?: boolean): Settings { return { @@ -135,7 +136,7 @@ test("capture strips properties not in the event's allowlist", async () => { }); await telemetry.flush(); expect(events().length).toBe(1); - const body = events()[0]!; + const body = defined(events()[0], "telemetry event"); expect(body.properties.status).toBe("ok"); expect(body.properties.turn_count).toBe(3); expect(body.properties.duration_ms).toBe(100); @@ -168,7 +169,7 @@ test("capture strips properties not in $ai_generation's allowlist", async () => }); await telemetry.flush(); expect(events().length).toBe(1); - const body = events()[0]!; + const body = defined(events()[0], "telemetry event"); expect(body.event).toBe("$ai_generation"); expect(body.properties.$ai_trace_id).toBe("trace-1"); expect(body.properties.$ai_provider).toBe("openai-compatible"); @@ -197,7 +198,7 @@ test("capture strips properties not in $ai_span's allowlist, including raw tool }); await telemetry.flush(); expect(events().length).toBe(1); - const body = events()[0]!; + const body = defined(events()[0], "telemetry event"); expect(body.event).toBe("$ai_span"); expect(body.properties.$ai_trace_id).toBe("trace-1"); expect(body.properties.$ai_span_id).toBe("span-1"); @@ -256,8 +257,8 @@ test("capture payload shape includes distinct_id and common props, with no clien telemetry.capture("cli_start"); await telemetry.flush(); expect(bodies.length).toBe(1); - expect(bodies[0]!.api_key).toBe("test-key"); - const body = events()[0]!; + expect(defined(bodies[0], "telemetry body").api_key).toBe("test-key"); + const body = defined(events()[0], "telemetry event"); expect(body.event).toBe("cli_start"); expect(body.properties.distinct_id).toBe("my-install-id"); expect(typeof body.timestamp).toBe("string"); @@ -289,7 +290,7 @@ test("intentional survey capture omits anonymous person-processing flag", async }), ).toBe(true); await telemetry.flush(); - const body = events()[0]!; + const body = defined(events()[0], "telemetry event"); expect(body.event).toBe("survey sent"); expect(body.properties.$process_person_profile).toBeUndefined(); }); @@ -321,7 +322,7 @@ test("flush resolves after pending captures settle", async () => { }); test("flush gives up after its deadline when a request never settles", async () => { - const impl = (() => new Promise(() => {})) as unknown as typeof fetch; + const impl = (() => new Promise(() => undefined)) as unknown as typeof fetch; const telemetry = createTelemetry({ settings: settingsWith("id"), env: {}, @@ -365,10 +366,10 @@ test("capture attaches the same session_id across multiple events in one process await telemetry.flush(); const captured = events(); expect(captured.length).toBe(2); - const sessionId = captured[0]!.properties.session_id; + const sessionId = defined(captured[0], "first event").properties.session_id; expect(typeof sessionId).toBe("string"); expect((sessionId as string).length).toBeGreaterThan(0); - expect(captured[1]!.properties.session_id).toBe(sessionId); + expect(defined(captured[1], "second event").properties.session_id).toBe(sessionId); expect(sessionId).toBe(getSessionId()); }); @@ -378,7 +379,7 @@ test("ensureTelemetrySettings called twice keeps installationId and enabled flag try { const first = await ensureTelemetrySettings(path); expect(typeof first.telemetry?.installationId).toBe("string"); - expect(first.telemetry?.installationId!.length).toBeGreaterThan(0); + expect(defined(first.telemetry?.installationId, "installationId").length).toBeGreaterThan(0); const second = await ensureTelemetrySettings(path); expect(second.telemetry?.installationId).toBe(first.telemetry?.installationId); @@ -454,7 +455,7 @@ test("reaching the batch size sends one request holding every queued event", asy telemetry.capture("session_end", { turn_count: 3 }); await new Promise((resolve) => setTimeout(resolve, 5)); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0]!)).toEqual([1, 2, 3]); + expect(turnCounts(defined(bodies[0], "telemetry body"))).toEqual([1, 2, 3]); }); test("a partial batch is sent once the batch interval elapses", async () => { @@ -471,7 +472,7 @@ test("a partial batch is sent once the batch interval elapses", async () => { await new Promise((resolve) => setTimeout(resolve, 60)); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0]!)).toEqual([1]); + expect(turnCounts(defined(bodies[0], "telemetry body"))).toEqual([1]); }); test("overflowing the queue drops the oldest events", async () => { @@ -486,7 +487,7 @@ test("overflowing the queue drops the oldest events", async () => { for (let turn = 1; turn <= 5; turn++) telemetry.capture("session_end", { turn_count: turn }); await telemetry.flush(); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0]!)).toEqual([3, 4, 5]); + expect(turnCounts(defined(bodies[0], "telemetry body"))).toEqual([3, 4, 5]); }); test("captures during a request queue behind it instead of opening a second one", async () => { @@ -525,7 +526,7 @@ test("flush drains a partially full queue within its deadline", async () => { await telemetry.flush(); expect(Date.now() - start).toBeLessThan(500); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0]!)).toEqual([1, 2]); + expect(turnCounts(defined(bodies[0], "telemetry body"))).toEqual([1, 2]); }); test("a hung endpoint caps the queue and never opens a second request", async () => { @@ -543,7 +544,7 @@ test("a hung endpoint caps the queue and never opens a second request", async () } expect(gate.bodies.length).toBe(1); expect(gate.peak()).toBe(1); - expect(turnCounts(gate.bodies[0]!)).toEqual([1, 2]); + expect(turnCounts(defined(gate.bodies[0], "telemetry body"))).toEqual([1, 2]); gate.openGate(); await telemetry.flush(); diff --git a/tests/unit/tui/agent-tools.test.ts b/tests/unit/tui/agent-tools.test.ts index e09f910f6..0186f9d06 100644 --- a/tests/unit/tui/agent-tools.test.ts +++ b/tests/unit/tui/agent-tools.test.ts @@ -7,7 +7,7 @@ import type { PermissionGate } from "../../../src/permission/gate.js"; import { mcpServerFingerprint } from "../../../src/trust/project-trust.js"; import { withMockedModule } from "../../helpers/mock-module.js"; -const mockDispose = mock(async () => {}); +const mockDispose = mock(async () => undefined); const mockPosixTools = { definitions: [ @@ -149,17 +149,17 @@ const fakePermissionGate: PermissionGate = { resolveSuspended: mock(async () => undefined), isReactorGated: () => false, getApprovals: () => [], - reset: () => {}, + reset: () => undefined, getSessionApprovals: () => [], - removeSessionApproval: () => {}, - setSeededApprovals: () => {}, + removeSessionApproval: () => undefined, + setSeededApprovals: () => undefined, getAuto: () => false, - setAuto: () => {}, + setAuto: () => undefined, getSkipPermissions: () => false, - setSkipPermissions: () => {}, - setProviderIdentity: () => {}, - registerMcpClient: mock(() => {}), - unregisterMcpServer: mock(() => {}), + setSkipPermissions: () => undefined, + setProviderIdentity: () => undefined, + registerMcpClient: mock(() => undefined), + unregisterMcpServer: mock(() => undefined), }; const callOperator = async ( @@ -428,8 +428,8 @@ test("headless MCP connection does not wait for interactive OAuth", async () => await toolset.connectMCP({ interactiveAuth: false, - onStatus: () => {}, - onToolsChanged: () => {}, + onStatus: () => undefined, + onToolsChanged: () => undefined, }); expect(mockConnectMCPServer).toHaveBeenCalledTimes(1); @@ -458,7 +458,7 @@ test("late connect of an untrusted local-source server does not spawn", async () await toolset.connectMCPServer(localStdioServer, { interactiveAuth: false, onStatus: (status) => statuses.push(status), - onToolsChanged: () => {}, + onToolsChanged: () => undefined, }); expect(mockConnectMCPServer).not.toHaveBeenCalled(); @@ -487,8 +487,8 @@ test("late connect of an untrusted local-source server fail-closes when requestM await toolset.connectMCPServer(localStdioServer, { interactiveAuth: false, - onStatus: () => {}, - onToolsChanged: () => {}, + onStatus: () => undefined, + onToolsChanged: () => undefined, }); expect(trustAsks).toBe(1); @@ -512,8 +512,8 @@ test("late connect of a trusted local-source server still connects", async () => await toolset.connectMCPServer(localStdioServer, { interactiveAuth: false, - onStatus: () => {}, - onToolsChanged: () => {}, + onStatus: () => undefined, + onToolsChanged: () => undefined, }); expect(mockConnectMCPServer).toHaveBeenCalledTimes(1); @@ -534,8 +534,8 @@ test("late connect of a global-source HTTP server does not require trust", async await toolset.connectMCPServer(globalHttpServer, { interactiveAuth: false, - onStatus: () => {}, - onToolsChanged: () => {}, + onStatus: () => undefined, + onToolsChanged: () => undefined, }); expect(mockConnectMCPServer).toHaveBeenCalledTimes(1); @@ -558,7 +558,7 @@ test("startup connectMCP still fail-closes untrusted local servers", async () => await toolset.connectMCP({ interactiveAuth: false, onStatus: (status) => statuses.push(status), - onToolsChanged: () => {}, + onToolsChanged: () => undefined, }); expect(mockConnectMCPServer).not.toHaveBeenCalled(); diff --git a/tests/unit/tui/at-mention-resolution.test.ts b/tests/unit/tui/at-mention-resolution.test.ts index de18e4748..f5c5048b1 100644 --- a/tests/unit/tui/at-mention-resolution.test.ts +++ b/tests/unit/tui/at-mention-resolution.test.ts @@ -236,7 +236,7 @@ describe("resolveAtMentions", () => { expect(resolved).toContain(`\`${join(worktree, "shared.ts")}\`:`); expect(resolved).toContain("export const shared = true;"); } finally { - await execFileAsync("git", ["worktree", "remove", "--force", worktree]).catch(() => {}); + await execFileAsync("git", ["worktree", "remove", "--force", worktree]).catch(() => undefined); await rm(repo, { recursive: true, force: true }); await rm(worktree, { recursive: true, force: true }); } diff --git a/tests/unit/tui/run-sink.test.ts b/tests/unit/tui/run-sink.test.ts index e88fb9334..c506e76b6 100644 --- a/tests/unit/tui/run-sink.test.ts +++ b/tests/unit/tui/run-sink.test.ts @@ -1,11 +1,12 @@ import { test, expect } from "bun:test"; import { EventEmitter } from "node:events"; import { createRunSink, getTUIRunSummaryStatus } from "../../../src/session/run-sink.js"; +import { defined } from "../../helpers/defined.js"; function makeArgs() { const emitter = new EventEmitter(); const hookManager = { - dispatchPostTurn: (_ctx: unknown) => {}, + dispatchPostTurn: (_ctx: unknown) => undefined, getStatuses: () => [ { id: "h1", @@ -74,7 +75,7 @@ test("getTurnCollector is available and has expected shape", () => { const args = makeArgs(); const runSink = createRunSink(args); // hooks are configured in makeArgs(), so the collector is non-null here - const collector = runSink.getTurnCollector()!; + const collector = defined(runSink.getTurnCollector(), "turn collector"); expect(typeof collector.observe).toBe("function"); expect(typeof collector.getTurns).toBe("function"); expect(typeof collector.getTokenUsage).toBe("function"); @@ -102,7 +103,7 @@ test("reset clears status, error, and turn collector between sessions", () => { // The turn collector returned after reset is fresh. // hooks are configured in makeArgs(), so the collector is non-null here - const collector = runSink.getTurnCollector()!; + const collector = defined(runSink.getTurnCollector(), "turn collector"); expect(collector.getTurns()).toHaveLength(0); expect(collector.getToolCallCount()).toBe(0); diff --git a/tests/unit/tui/runner.test.ts b/tests/unit/tui/runner.test.ts index 846e064ac..797821e2b 100644 --- a/tests/unit/tui/runner.test.ts +++ b/tests/unit/tui/runner.test.ts @@ -10,6 +10,7 @@ import { import { createTUIEventEmitter, getTUIRunSummaryStatus } from "../../../src/tui/runner/index.js"; import { loadLocalSettingsWriteBase } from "../../../src/tui/runner/settings.js"; import { tuiSendFailureMessage } from "../../../src/tui/runner/send-failure-message.js"; +import { defined } from "../../helpers/defined.js"; import { createSessionOperationQueue } from "../../../src/tui/session-operation-queue.js"; import { createRunSink } from "../../../src/session/run-sink.js"; @@ -123,7 +124,7 @@ test("loadLocalSettingsWriteBase distinguishes absent from unreadable", async () test("rotation resets run-sink so a new session starts from a clean state", () => { const emitter = new EventEmitter(); const hookManager = { - dispatchPostTurn: () => {}, + dispatchPostTurn: () => undefined, getStatuses: () => [ { id: "h1", @@ -146,7 +147,7 @@ test("rotation resets run-sink so a new session starts from a clean state", () = // The new collector is a fresh instance — not the same object as before. // hooks are configured above, so the collector is non-null here - const collectorAfterReset = runSink.getTurnCollector()!; + const collectorAfterReset = defined(runSink.getTurnCollector(), "turn collector"); expect(collectorAfterReset).not.toBe(collectorBeforeReset); // Status is cancelled (no events received in new session yet). @@ -215,7 +216,7 @@ test("a failed close followed by a lock error never surfaces as a raw AgentConte } expect(rebuildError).not.toBeNull(); expect(rebuildError).not.toBeInstanceOf(AgentContextLockError); - expect(rebuildError!.message).toMatch(/restart/i); + expect(defined(rebuildError, "rebuild error").message).toMatch(/restart/i); }); // reloadIfIdle itself is a closure captured inside runTUI's single ~2500-line @@ -272,7 +273,7 @@ test("a rejecting reload op through the real session-operation-queue never trigg expect(unhandled).toBeNull(); expect(fatalBuildError).not.toBeNull(); expect(fatalBuildError).not.toBeInstanceOf(AgentContextLockError); - expect(fatalBuildError!.message).toMatch(/restart/i); + expect(defined(fatalBuildError, "fatal build error").message).toMatch(/restart/i); }); // A true negative control (reproducing reloadIfIdle's pre-fix shape — no diff --git a/tests/unit/vendor-patch-ledger.test.ts b/tests/unit/vendor-patch-ledger.test.ts index e875f2ca2..e4508b66b 100644 --- a/tests/unit/vendor-patch-ledger.test.ts +++ b/tests/unit/vendor-patch-ledger.test.ts @@ -11,6 +11,7 @@ import { readdir, readFile, stat } from "node:fs/promises"; import { join, relative } from "node:path"; import { describe, expect, test } from "bun:test"; +import { defined } from "../helpers/defined.js"; const repoRoot = join(import.meta.dirname, "../.."); const vendorRoot = join(repoRoot, "vendor"); @@ -74,7 +75,7 @@ async function collectMarkers(pkgDir: string): Promise { while ((match = MARKER_RE.exec(text)) !== null) { const before = text.slice(0, match.index); const line = before.split("\n").length; - markers.push({ file: rel, anchor: match[2]!, line }); + markers.push({ file: rel, anchor: defined(match[2], "marker anchor"), line }); } } return markers; @@ -86,7 +87,7 @@ async function collectHeadings(ledgerPath: string): Promise { let match: RegExpExecArray | null; HEADING_RE.lastIndex = 0; while ((match = HEADING_RE.exec(text)) !== null) { - headings.push(match[1]!); + headings.push(defined(match[1], "ledger heading")); } return headings; } diff --git a/tests/unit/workflow-host.test.ts b/tests/unit/workflow-host.test.ts index 7fd1363d2..16006e3a7 100644 --- a/tests/unit/workflow-host.test.ts +++ b/tests/unit/workflow-host.test.ts @@ -9,6 +9,7 @@ import { WorkflowCoordinator } from "../../src/workflows/coordinator.js"; import { WorkflowHost } from "../../src/workflows/host.js"; import { findWorkflow } from "../../src/workflows/index.js"; import { WorkflowRuntime } from "../../src/workflows/runtime.js"; +import { defined } from "../helpers/defined.js"; import { flushWorkflowStateWrites, saveWorkflowState } from "../../src/workflows/state.js"; function tool(name: string): ToolDefinition { @@ -22,7 +23,7 @@ function drain( while (host.isActive()) { const stepId = director.coordinator?.currentStepId(); expect(stepId).not.toBeNull(); - expect(host.complete(stepId!)).toBe("advanced"); + expect(host.complete(defined(stepId, "stepId"))).toBe("advanced"); } } @@ -112,7 +113,7 @@ test("reset detaches the workflow", async () => { test("directive uses submit_output with the current step id", async () => { await withHost([], async (host, director) => { host.start("build"); - const coordinator = director.coordinator!; + const coordinator = defined(director.coordinator, "coordinator"); expect(coordinator).toBeDefined(); const directive = coordinator.directive(); expect(directive).not.toBeNull(); @@ -129,8 +130,8 @@ test("complete() advances the current step and records history", async () => { expect(host.isActive()).toBe(false); const history = host.history(); expect(history).toHaveLength(1); - expect(history[0]!.name).toBe("review"); - expect(history[0]!.steps.length).toBeGreaterThan(0); + expect(defined(history[0], "history entry").name).toBe("review"); + expect(defined(history[0], "history entry").steps.length).toBeGreaterThan(0); }); }); @@ -159,14 +160,14 @@ test("resume() uses the same completion listener as a fresh start", async () => const workflow = findWorkflow("review"); expect(workflow).toBeDefined(); const runtime = new WorkflowRuntime(new Map()); - runtime.start(workflow!); + runtime.start(defined(workflow, "workflow")); await saveWorkflowState(cwd, "session-1", runtime.state(), home); await host.resume(); expect(host.isActive()).toBe(true); drain(host, director); expect(host.history()).toHaveLength(1); - expect(host.history()[0]!.name).toBe("review"); + expect(defined(host.history()[0], "history entry").name).toBe("review"); }); }); @@ -175,7 +176,7 @@ test("resume() restores an on-disk workflow snapshot for the session", async () const workflow = findWorkflow("review"); expect(workflow).toBeDefined(); const runtime = new WorkflowRuntime(new Map()); - runtime.start(workflow!); + runtime.start(defined(workflow, "workflow")); runtime.advance(); await saveWorkflowState(cwd, "session-1", runtime.state(), home); diff --git a/tests/unit/workflows-definitions.test.ts b/tests/unit/workflows-definitions.test.ts index 7445c27d8..a6076d7fb 100644 --- a/tests/unit/workflows-definitions.test.ts +++ b/tests/unit/workflows-definitions.test.ts @@ -4,6 +4,7 @@ import type { ToolDefinition } from "@intx/types/runtime"; import { WorkflowRuntime } from "../../src/workflows/runtime.js"; import { findWorkflow } from "../../src/workflows/index.js"; import { detectCapabilities, type CapabilityMap } from "../../src/workflows/capabilities.js"; +import { defined } from "../helpers/defined.js"; function tool(name: string): ToolDefinition { return { name, description: name, inputSchema: { type: "object", properties: {} } }; @@ -24,7 +25,7 @@ function drive(name: string, caps: CapabilityMap): string[] { runtime.start(workflow); const ids: string[] = []; for (let i = 0; i < 200 && runtime.currentStep() !== null; i++) { - ids.push(runtime.currentStep()!.id); + ids.push(defined(runtime.currentStep(), "current step").id); runtime.advance(); } expect(runtime.isComplete()).toBe(true); diff --git a/tests/unit/workflows-director.test.ts b/tests/unit/workflows-director.test.ts index a8d6c42b1..25e526d6a 100644 --- a/tests/unit/workflows-director.test.ts +++ b/tests/unit/workflows-director.test.ts @@ -94,7 +94,7 @@ test("the active step directive is injected into the inferred system prompt", as runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE PROMPT", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); @@ -130,7 +130,7 @@ test("a submit_output tool call with the current step id advances the runtime th runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); const caps = makeCapabilities(); @@ -166,7 +166,7 @@ test("a stale submit_output does not skip ahead through the director", async () runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); const caps = makeCapabilities(); @@ -250,7 +250,7 @@ test("auto-continuation fires on reply() as well as wait() after a text turn", a runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); const caps = makeCapabilities(); @@ -311,7 +311,7 @@ test("a content-free workflow turn with open tasks nudges toward submit_output", runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); const caps = makeCapabilities(); @@ -337,7 +337,7 @@ test("open tasks do not defeat the workflow stuck-cutoff after 3 idle turns", as runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); const caps = makeCapabilities(); @@ -358,7 +358,7 @@ test("auto-continuation falls back after 3 consecutive text-only turns", async ( runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); const caps = makeCapabilities(); @@ -379,7 +379,7 @@ test("after spacer echo-cap a non-gate workflow step does not empty-settle", asy runtime.start(flow); const coordinator = new WorkflowCoordinator(runtime); const director = createChatDirector("BASE", [], { - onTasksChange: () => {}, + onTasksChange: () => undefined, workflowCoordinator: coordinator, }); const caps = makeCapabilities(); diff --git a/tests/unit/workflows-runtime-persistence.test.ts b/tests/unit/workflows-runtime-persistence.test.ts index dcc06522e..aaabd5614 100644 --- a/tests/unit/workflows-runtime-persistence.test.ts +++ b/tests/unit/workflows-runtime-persistence.test.ts @@ -7,6 +7,7 @@ import "../helpers/workflows.js"; import { findWorkflow } from "../../src/workflows/index.js"; import { WorkflowRuntime } from "../../src/workflows/runtime.js"; import { loadWorkflowState, saveWorkflowState } from "../../src/workflows/state.js"; +import { defined } from "../helpers/defined.js"; test("WorkflowRuntime resumes from workflow.json written mid sub-workflow chain", async () => { const cwd = await mkdtemp(join(tmpdir(), "wf-runtime-persist-")); @@ -16,7 +17,7 @@ test("WorkflowRuntime resumes from workflow.json written mid sub-workflow chain" expect(build).toBeDefined(); const runtime = new WorkflowRuntime(new Map()); - runtime.start(build!); + runtime.start(defined(build, "build workflow")); const first = runtime.currentStep()?.id; runtime.advance(); const mid = runtime.currentStep()?.id; @@ -29,7 +30,7 @@ test("WorkflowRuntime resumes from workflow.json written mid sub-workflow chain" expect(loaded).toEqual(runtime.state()); const resumed = new WorkflowRuntime(new Map()); - resumed.restore(loaded!); + resumed.restore(defined(loaded, "loaded workflow state")); expect(resumed.currentStep()?.id).toBe(mid); resumed.advance(); expect(resumed.isActive()).toBe(true); From ff5e107296b9c62514ead6dee0a559c31f8c08ff Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 22:40:08 -0700 Subject: [PATCH 06/10] Fail oxlint on non-null assertions and empty functions --- .oxlintrc.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 2ceba0abb..eb36056c9 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -63,7 +63,7 @@ "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": "off", + "typescript/no-non-null-assertion": "error", "typescript/no-this-alias": "error", "typescript/no-unnecessary-type-constraint": "error", "typescript/no-unsafe-declaration-merging": "error", @@ -75,7 +75,7 @@ "typescript/prefer-literal-enum-member": "error", "typescript/prefer-namespace-keyword": "error", "typescript/unified-signatures": "error", - "no-empty-function": "off" + "no-empty-function": "error" }, "overrides": [ { From 00832c6eab5941152bcfc5e272f3d5dea068516e Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 22:40:22 -0700 Subject: [PATCH 07/10] Reformat the tree with oxfmt --- docs/MCP.md | 4 +- eslint.config.js | 8 +- evals/capability/README.md | 12 +- evals/capability/behaviors.test.ts | 77 +- evals/capability/behaviors.ts | 30 +- evals/capability/lib.test.ts | 207 ++++- evals/capability/lib.ts | 211 +++-- .../src/providers.test.ts | 16 +- .../first-class-providers/src/providers.ts | 22 +- packages/first-class-providers/src/types.ts | 7 +- packages/opencode-go/src/auth.ts | 4 +- packages/opencode-go/src/constants.ts | 3 +- packages/opencode-go/src/endpoint.test.ts | 39 +- packages/opencode-go/src/endpoint.ts | 5 +- packages/opencode-go/src/errors.ts | 37 +- packages/opencode-go/src/identity.test.ts | 19 +- packages/opencode-go/src/identity.ts | 10 +- packages/opencode-go/src/index.ts | 6 +- packages/opencode-go/src/models.ts | 22 +- packages/opencode-go/src/usage.ts | 33 +- .../corbits-skills/skills/typescript/SKILL.md | 24 +- scripts/approval-forensics.ts | 29 +- scripts/eval-capability.test.ts | 149 +++- scripts/eval-capability.ts | 168 +++- scripts/eval-public-swe-one.test.ts | 8 +- scripts/eval-public-swe-one.ts | 91 ++- scripts/generate-homebrew-tap.ts | 12 +- scripts/guard-real-projects-dir.ts | 5 +- scripts/intervention-forensics.ts | 47 +- scripts/oxlint-plugin-corbits.js | 10 +- scripts/test-paths.ts | 10 +- scripts/tool-fingerprint-forensics.ts | 16 +- src/agent/agent-search.test.ts | 34 +- src/agent/agent-search.ts | 31 +- src/agent/apply-patch-diff.test.ts | 43 +- src/agent/background-shell-tool.test.ts | 26 +- src/agent/background-shell-tool.ts | 14 +- src/agent/codex-apply-patch.test.ts | 10 +- src/agent/codex-apply-patch.ts | 78 +- src/agent/codex-read-raw-file.ts | 21 +- src/agent/codex-tool-mount.test.ts | 38 +- src/agent/codex-tool-proxies.test.ts | 109 ++- src/agent/codex-tool-proxies.ts | 68 +- src/agent/compaction.test.ts | 339 ++++++-- src/agent/compaction.ts | 49 +- src/agent/context-estimate.test.ts | 42 +- src/agent/context-estimate.ts | 27 +- src/agent/context-extensions.ts | 21 +- src/agent/director.test.ts | 111 ++- src/agent/director.ts | 234 ++++-- src/agent/directors/bake-skills.test.ts | 25 +- src/agent/directors/bake-skills.ts | 4 +- .../directors/bruckheimer/package.test.ts | 12 +- src/agent/directors/bruckheimer/package.ts | 6 +- src/agent/directors/builder/package.test.ts | 12 +- src/agent/directors/builder/package.ts | 11 +- src/agent/directors/counsel/package.test.ts | 18 +- src/agent/directors/critic/package.test.ts | 20 +- src/agent/directors/emil/package.test.ts | 16 +- src/agent/directors/gaasbot/package.test.ts | 17 +- src/agent/directors/greybeard/package.test.ts | 20 +- src/agent/directors/identity.test.ts | 43 +- src/agent/directors/identity.ts | 4 +- src/agent/directors/index.ts | 5 +- src/agent/directors/intern/package.test.ts | 12 +- src/agent/directors/intern/package.ts | 3 +- src/agent/directors/neckbeard/package.test.ts | 10 +- src/agent/directors/rand/package.test.ts | 8 +- src/agent/directors/registry.test.ts | 17 +- src/agent/directors/registry.ts | 65 +- .../directors/shakespeare/package.test.ts | 8 +- src/agent/directors/skywalker/package.test.ts | 52 +- src/agent/directors/skywalker/package.ts | 6 +- src/agent/directors/tester/package.ts | 3 +- src/agent/directors/testsmith/package.test.ts | 4 +- src/agent/directors/tool-sets.test.ts | 21 +- src/agent/directors/tool-sets.ts | 18 +- src/agent/directors/types.ts | 15 +- src/agent/environment.test.ts | 15 +- src/agent/environment.ts | 24 +- src/agent/exa-web-fetch-alias.test.ts | 263 +++++-- src/agent/fleet-verbs-mount.test.ts | 22 +- src/agent/lazy-blob-reader.test.ts | 45 +- src/agent/lazy-blob-reader.ts | 8 +- src/agent/live-tool-dispatch.test.ts | 10 +- src/agent/live-tool-dispatch.ts | 15 +- src/agent/lsp-availability.test.ts | 13 +- src/agent/lsp-availability.ts | 15 +- src/agent/message-provenance.ts | 4 +- src/agent/model-family-policy.test.ts | 25 +- src/agent/model-family-policy.ts | 10 +- src/agent/posix-tool-plugins.test.ts | 65 +- src/agent/posix-tool-plugins.ts | 13 +- src/agent/product-mutation-tools.test.ts | 15 +- src/agent/product-mutation-tools.ts | 4 +- src/agent/profiles.ts | 18 +- src/agent/prompts.test.ts | 29 +- src/agent/prompts.ts | 69 +- src/agent/reactor-events.test.ts | 4 +- src/agent/reactor-events.ts | 6 +- src/agent/renderer.ts | 45 +- src/agent/retry-policy.test.ts | 36 +- src/agent/retry-policy.ts | 31 +- src/agent/skill-search.test.ts | 9 +- src/agent/skill-search.ts | 22 +- src/agent/tasks.ts | 27 +- src/agent/tool-classification.test.ts | 9 +- src/agent/tool-classification.ts | 9 +- src/agent/tool-schema-normalize.test.ts | 21 +- src/agent/tool-schema-normalize.ts | 17 +- src/agent/tool-search.test.ts | 92 ++- src/agent/tool-search.ts | 39 +- src/agent/tools-mcp-disconnect.test.ts | 169 ++-- src/agent/tools.ts | 229 ++++-- src/agent/use-skill.ts | 14 +- src/auth/callback-page.test.ts | 21 +- src/auth/callback-page.ts | 10 +- src/auth/codex/constants.ts | 7 +- src/auth/codex/index.ts | 12 +- src/auth/codex/login.ts | 4 +- src/auth/codex/oauth.ts | 14 +- src/auth/codex/session.ts | 23 +- src/auth/codex/store.ts | 6 +- src/auth/codex/usage-limit-error.test.ts | 14 +- src/auth/codex/usage-limit-error.ts | 24 +- src/auth/codex/usage.ts | 55 +- src/auth/oauth-scope-check.test.ts | 53 +- src/auth/oauth-scope-check.ts | 37 +- src/auth/oauth/browser.ts | 7 +- src/auth/oauth/callback-server.ts | 25 +- src/auth/oauth/client.ts | 15 +- src/auth/oauth/login.ts | 6 +- src/auth/oauth/oauth.test.ts | 169 +++- src/auth/oauth/pkce.ts | 6 +- src/auth/oauth/session.ts | 26 +- src/auth/oauth/store.ts | 60 +- src/auth/xai/callback-server.ts | 4 +- src/auth/xai/constants.ts | 6 +- src/auth/xai/index.ts | 12 +- src/auth/xai/login.ts | 4 +- src/auth/xai/oauth.test.ts | 52 +- src/auth/xai/oauth.ts | 10 +- src/auth/xai/session.ts | 16 +- src/auth/xai/store.ts | 6 +- src/auth/xai/usage.ts | 32 +- src/changelog/index.test.ts | 13 +- src/changelog/index.ts | 50 +- src/config.test.ts | 431 +++++++--- src/config/bifrost.ts | 5 +- src/config/codex-providers.ts | 12 +- src/config/index.ts | 251 ++++-- src/config/inference-sources.ts | 33 +- src/config/oauth-providers.test.ts | 12 +- src/config/oauth-providers.ts | 18 +- src/config/profiles.ts | 5 +- src/config/providers.test.ts | 17 +- src/config/providers.ts | 33 +- src/config/session-mode.test.ts | 15 +- src/config/settings.ts | 317 ++++++-- src/config/xai-providers.test.ts | 6 +- src/context-compactor.test.ts | 491 +++++++++--- src/cost/cost-summary.test.ts | 16 +- src/cost/cost-summary.ts | 29 +- src/cost/cost-visibility.test.ts | 92 ++- src/cost/cost-visibility.ts | 26 +- src/cost/faremeter.ts | 12 +- src/cost/pricing-fetcher.ts | 57 +- src/cost/pricing-metadata.ts | 8 +- src/cost/session-cost.test.ts | 55 +- src/cost/session-cost.ts | 24 +- src/crash/report.test.ts | 28 +- src/crash/report.ts | 9 +- src/director.test.ts | 736 ++++++++++++++---- src/exec/runner.ts | 228 ++++-- src/extensions/skills.ts | 18 +- src/index.ts | 59 +- src/inference-abort.test.ts | 3 +- src/inference-abort.ts | 14 +- src/inference-error-message.test.ts | 20 +- src/inference-error-message.ts | 98 ++- src/inference-gateway-error.test.ts | 41 +- src/inference-gateway-error.ts | 112 ++- src/list-dir.test.ts | 8 +- src/logging/sink.test.ts | 8 +- src/logging/sink.ts | 6 +- src/mcp/add-server.test.ts | 297 +++++-- src/mcp/add-server.ts | 52 +- src/mcp/auth-store.test.ts | 78 +- src/mcp/auth-store.ts | 35 +- src/mcp/callback-server.test.ts | 50 +- src/mcp/callback-server.ts | 22 +- src/mcp/client-auth-policy.test.ts | 24 +- src/mcp/client-auth-reauth-cap.test.ts | 245 ++++-- src/mcp/client.ts | 199 +++-- src/mcp/exa.ts | 3 +- src/mcp/oauth-provider.test.ts | 147 +++- src/mcp/oauth-provider.ts | 56 +- src/mcp/plugin.test.ts | 13 +- src/mcp/plugin.ts | 21 +- src/mcp/tool-name.test.ts | 13 +- src/mcp/tool-name.ts | 28 +- src/mcp/tool-permissions.test.ts | 5 +- src/mcp/tool-permissions.ts | 10 +- src/perf/assert-spans.test.ts | 92 ++- src/perf/assert-spans.ts | 27 +- src/perf/attribution-report.test.ts | 39 +- src/perf/attribution-report.ts | 80 +- src/perf/dump.ts | 5 +- src/perf/fixtures/multi-tool-turn.ts | 8 +- src/perf/index.test.ts | 24 +- src/perf/index.ts | 7 +- src/perf/otel-config.test.ts | 128 ++- src/perf/otel-config.ts | 71 +- src/perf/otel-sink.test.ts | 84 +- src/perf/otel-sink.ts | 27 +- src/perf/permission-subagent-spans.test.ts | 30 +- src/perf/reactor-spans.test.ts | 119 ++- src/perf/reactor-spans.ts | 22 +- src/perf/rollup.test.ts | 59 +- src/perf/rollup.ts | 18 +- src/perf/sanitize.ts | 6 +- src/permission/admin.ts | 27 +- src/permission/approval-log.test.ts | 33 +- src/permission/approval-log.ts | 17 +- src/permission/authz-grants.ts | 26 +- src/permission/auto-shell-policy.test.ts | 75 +- src/permission/auto-shell-policy.ts | 52 +- src/permission/classify-security.test.ts | 582 +++++++++----- src/permission/classify.ts | 106 ++- src/permission/command.test.ts | 40 +- src/permission/command.ts | 20 +- src/permission/critique-grep-file-env.test.ts | 10 +- src/permission/decline-markers.ts | 6 +- src/permission/gate.test.ts | 30 +- src/permission/gate.ts | 182 ++++- src/permission/grant-scope.test.ts | 45 +- src/permission/path-restriction.ts | 18 +- src/permission/permission.test.ts | 668 ++++++++++++---- src/permission/queue.test.ts | 43 +- src/permission/queue.ts | 17 +- src/permission/reactor-authorize.test.ts | 157 ++-- src/permission/reactor-authorize.ts | 41 +- src/permission/store.test.ts | 56 +- src/permission/store.ts | 63 +- src/permission/types.ts | 4 +- src/permission/workspace-containment.test.ts | 51 +- src/permission/worktree-roots.test.ts | 3 +- src/plugins/admin.ts | 10 +- src/plugins/agent-plugins.test.ts | 60 +- src/plugins/agent-plugins.ts | 17 +- src/plugins/authz-plugin.test.ts | 159 +++- src/plugins/bounded-grep-fallback.test.ts | 16 +- src/plugins/bounded-grep-fallback.ts | 43 +- src/plugins/change-diff.ts | 5 +- src/plugins/claude-plugins.test.ts | 82 +- src/plugins/data-only-agent.test.ts | 6 +- src/plugins/data-only-agent.ts | 109 ++- src/plugins/data-only-commands.ts | 25 +- src/plugins/data-only.ts | 39 +- src/plugins/delete-file-plugin.test.ts | 76 +- src/plugins/delete-file-plugin.ts | 28 +- src/plugins/diagnostics.test.ts | 25 +- src/plugins/diagnostics.ts | 16 +- .../edit-file-diagnostics-plugin.test.ts | 80 +- src/plugins/edit-file-diagnostics-plugin.ts | 33 +- src/plugins/edit-file-line-range-plugin.ts | 5 +- src/plugins/edit-file-line-range.test.ts | 27 +- src/plugins/edit-file-line-range.ts | 49 +- src/plugins/file-mutation-lock.ts | 5 +- src/plugins/frontmatter.ts | 6 +- src/plugins/loader.test.ts | 7 +- src/plugins/loader.ts | 146 +++- src/plugins/lsp-hint-plugin.ts | 11 +- src/plugins/path-escape-plugin.test.ts | 62 +- src/plugins/path-escape-plugin.ts | 4 +- src/plugins/permission-plugin.test.ts | 145 +++- src/plugins/permission-plugin.ts | 11 +- src/plugins/read-file-guard-plugin.test.ts | 170 +++- src/plugins/read-file-guard-plugin.ts | 97 ++- src/plugins/register.ts | 12 +- src/plugins/result-truncation-plugin.test.ts | 122 ++- src/plugins/result-truncation-plugin.ts | 42 +- src/plugins/rg-output.test.ts | 13 +- src/plugins/rg-run.test.ts | 33 +- src/plugins/rg-run.ts | 22 +- src/plugins/ripgrep-plugin.ts | 62 +- src/plugins/secret-guard-plugin.test.ts | 37 +- src/plugins/secret-guard-plugin.ts | 15 +- src/plugins/secret-guard-symlink.test.ts | 40 +- src/plugins/shell-guard-plugin.test.ts | 227 ++++-- src/plugins/shell-guard-plugin.ts | 138 +++- src/plugins/skill-commands.ts | 3 +- src/plugins/tool-output-uri-plugin.test.ts | 10 +- src/plugins/tool-output-uri-plugin.ts | 10 +- src/plugins/tool-plugins.test.ts | 13 +- src/plugins/tool-plugins.ts | 17 +- src/plugins/tool-result-materialize.test.ts | 14 +- src/plugins/tool-result-materialize.ts | 13 +- src/plugins/tool-result-secret-scrub.test.ts | 35 +- src/plugins/tool-result-secret-scrub.ts | 11 +- src/plugins/tool-time-budget.ts | 15 +- src/plugins/uninstall.test.ts | 104 ++- src/plugins/uninstall.ts | 64 +- src/plugins/verify-plugin.test.ts | 60 +- src/plugins/verify-plugin.ts | 32 +- src/pricing-fetcher.test.ts | 53 +- src/pricing-metadata.test.ts | 6 +- src/profiles.test.ts | 26 +- src/prompts.test.ts | 68 +- src/provider/bifrost-adapter.test.ts | 9 +- src/provider/bifrost-adapter.ts | 12 +- src/provider/billing-product.test.ts | 18 +- src/provider/billing-product.ts | 15 +- src/provider/codex-responses-adapter.test.ts | 124 ++- src/provider/codex-responses-adapter.ts | 142 +++- src/provider/context-window.test.ts | 6 +- src/provider/context-window.ts | 10 +- src/provider/grok-responses-adapter.test.ts | 43 +- src/provider/grok-responses-adapter.ts | 41 +- src/provider/inference-dependencies.ts | 11 +- src/provider/models-endpoint.ts | 13 +- src/provider/ollama.test.ts | 83 +- src/provider/ollama.ts | 27 +- .../openai-compatible-adapter.test.ts | 52 +- src/provider/openai-compatible-adapter.ts | 6 +- src/provider/openai-responses-adapter.ts | 47 +- src/provider/opencode-go-adapter.test.ts | 21 +- src/provider/opencode-go-adapter.ts | 26 +- .../opencode-go-anthropic-adapter.test.ts | 21 +- src/provider/opencode-go-anthropic-adapter.ts | 11 +- src/provider/opencode-go-models.test.ts | 42 +- src/provider/opencode-go-models.ts | 24 +- src/provider/reasoning-effort.test.ts | 71 +- src/provider/reasoning-effort.ts | 91 ++- src/provider/replay-sanitizer.test.ts | 55 +- src/provider/replay-sanitizer.ts | 26 +- src/provider/validate-connection.test.ts | 18 +- src/provider/validate-connection.ts | 8 +- src/renderer.test.ts | 115 ++- src/session/approval-resume.ts | 51 +- src/session/assemble-runtime.test.ts | 52 +- src/session/assemble-runtime.ts | 59 +- src/session/attachment-store.test.ts | 32 +- src/session/attachment-store.ts | 4 +- src/session/attachment-uri.ts | 13 +- src/session/compactor.ts | 237 ++++-- src/session/hooks.test.ts | 13 +- src/session/hooks.ts | 47 +- src/session/incremental-jsonl.test.ts | 25 +- src/session/incremental-jsonl.ts | 44 +- src/session/index.ts | 39 +- src/session/list-sessions.test.ts | 23 +- src/session/live-model-switch.test.ts | 52 +- src/session/live-model-switch.ts | 5 +- src/session/optimized-context-store.test.ts | 277 +++++-- src/session/optimized-context-store.ts | 106 ++- src/session/project-key.test.ts | 28 +- src/session/project-key.ts | 9 +- src/session/rename-session.test.ts | 11 +- src/session/run-sink.test.ts | 134 +++- src/session/run-sink.ts | 43 +- src/session/runtime-assembly.test.ts | 97 ++- src/session/runtime-assembly.ts | 85 +- src/session/sent-messages.test.ts | 5 +- src/session/sent-messages.ts | 16 +- src/session/session-dir.test.ts | 6 +- src/session/session-label.test.ts | 4 +- src/session/session-label.ts | 7 +- src/session/state.test.ts | 26 +- src/session/state.ts | 23 +- src/session/stream-journal.test.ts | 26 +- src/session/stream-journal.ts | 34 +- src/session/summarizer.ts | 22 +- src/settings.test.ts | 452 ++++++++--- src/shell/background-shell.test.ts | 14 +- src/shell/background-shell.ts | 22 +- src/shell/command-segments.ts | 3 +- src/shell/persistent-shell-cwd.test.ts | 21 +- src/shell/persistent-shell-cwd.ts | 20 +- src/shell/run-shell-authz.test.ts | 302 +++++-- src/shell/run-shell-authz.ts | 149 +++- src/state.test.ts | 22 +- src/subagent/admission.ts | 4 +- src/subagent/agent-fleet.test.ts | 689 ++++++++++++---- src/subagent/agent-fleet.ts | 357 +++++++-- src/subagent/ask-director.test.ts | 24 +- src/subagent/ask-director.ts | 23 +- src/subagent/authority.test.ts | 113 ++- src/subagent/authority.ts | 5 +- src/subagent/dispose.ts | 29 +- src/subagent/fleet-dry-drive.test.ts | 27 +- src/subagent/fleet-dry-drive.ts | 27 +- src/subagent/fleet-report.ask-wake.test.ts | 6 +- src/subagent/fleet-report.test.ts | 60 +- src/subagent/fleet-report.ts | 58 +- src/subagent/followup-live-agent.test.ts | 46 +- src/subagent/identity-context.ts | 5 +- src/subagent/index.test.ts | 214 +++-- src/subagent/inference-auth-failure.test.ts | 8 +- src/subagent/inference-auth-failure.ts | 8 +- src/subagent/intervention-log.test.ts | 9 +- src/subagent/intervention-log.ts | 12 +- src/subagent/lifecycle-tools.test.ts | 310 ++++++-- src/subagent/lifecycle-tools.ts | 87 ++- src/subagent/lifecycle.test.ts | 44 +- src/subagent/lifecycle.ts | 31 +- src/subagent/nudge-director.test.ts | 326 ++++++-- src/subagent/nudge-director.ts | 86 +- src/subagent/provider-family.test.ts | 107 ++- src/subagent/provider-family.ts | 21 +- src/subagent/refresh-inference-source.test.ts | 13 +- src/subagent/refresh-inference-source.ts | 3 +- src/subagent/report.ts | 38 +- src/subagent/retain-salvage.test.ts | 9 +- src/subagent/run-authority.test.ts | 23 +- src/subagent/run-codex-proxy.test.ts | 38 +- src/subagent/run-persist-close.test.ts | 59 +- .../run-resolved-provider-failure.test.ts | 151 +++- src/subagent/run-settlement.test.ts | 11 +- src/subagent/run-suspended-send.test.ts | 9 +- src/subagent/run.ts | 311 ++++++-- src/subagent/session-store.test.ts | 373 +++++++-- src/subagent/session-store.ts | 312 ++++++-- src/subagent/shell-evidence.test.ts | 24 +- src/subagent/shell-evidence.ts | 12 +- src/subagent/spawn-agent-worktree.test.ts | 55 +- src/subagent/stop-policy.ts | 52 +- src/subagent/submit-result.test.ts | 5 +- src/subagent/submit-result.ts | 5 +- src/subagent/thrash.test.ts | 32 +- src/subagent/thrash.ts | 16 +- src/subagent/tool-preview.test.ts | 36 +- src/subagent/tool-preview.ts | 27 +- src/subagent/trace-reader.test.ts | 60 +- src/subagent/trace-reader.ts | 71 +- src/subagent/trace-tool.test.ts | 33 +- src/subagent/trace-tool.ts | 40 +- src/subagent/types.ts | 9 +- src/subagent/worktree.test.ts | 42 +- src/subagent/worktree.ts | 22 +- src/telemetry/ai-observability.test.ts | 66 +- src/telemetry/ai-observability.ts | 30 +- src/telemetry/classify.ts | 9 +- src/telemetry/feedback.test.ts | 11 +- src/telemetry/feedback.ts | 37 +- src/telemetry/first-run.ts | 27 +- src/telemetry/index.ts | 69 +- src/telemetry/product-events.ts | 32 +- src/telemetry/singleton.ts | 3 +- src/telemetry/toggle.ts | 34 +- src/tools/eval-http-env.ts | 4 +- src/tools/html-convert.test.ts | 3 +- src/tools/html-convert.ts | 27 +- src/tools/ssrf-guard.test.ts | 19 +- src/tools/ssrf-guard.ts | 9 +- src/tools/web-fetch.test.ts | 26 +- src/tools/web-fetch.ts | 43 +- src/tools/web-search.test.ts | 30 +- src/tools/web-search.ts | 28 +- src/trust/path-trust.ts | 14 +- src/trust/project-trust.test.ts | 77 +- src/trust/project-trust.ts | 15 +- src/tui/README.md | 4 +- src/tui/agent-ask-wake.test.ts | 79 +- src/tui/agent-progress.test.ts | 36 +- src/tui/agent-progress.ts | 32 +- src/tui/agent-source-sync.ts | 5 +- src/tui/approval-prompt-visibility.test.ts | 20 +- src/tui/chrome-repaint.test.ts | 12 +- src/tui/chrome-state.test.ts | 132 +++- src/tui/chrome-state.ts | 95 ++- src/tui/collapse.test.ts | 64 +- src/tui/command-catalog.test.ts | 26 +- src/tui/command-catalog.ts | 9 +- src/tui/command-display.test.ts | 207 +++-- src/tui/command-display.ts | 24 +- src/tui/command-registry-setup.test.ts | 10 +- src/tui/command-surfaces.test.ts | 289 +++++-- src/tui/command-surfaces.ts | 334 ++++++-- src/tui/commands/built-in.test.ts | 43 +- src/tui/commands/built-in.ts | 51 +- src/tui/commands/registry.test.ts | 53 +- src/tui/commands/registry.ts | 17 +- src/tui/components/at-mention/list.test.ts | 8 +- src/tui/components/at-mention/list.ts | 10 +- src/tui/components/at-mention/parse.test.ts | 15 +- .../prompt-action-bar-label.test.ts | 26 +- src/tui/copy-path.test.ts | 30 +- src/tui/copy-path.ts | 17 +- src/tui/copy-wire.test.ts | 34 +- src/tui/correlation-acceptance.test.ts | 5 +- src/tui/correlation-acceptance.ts | 19 +- src/tui/decision-truncation.test.ts | 26 +- src/tui/deliver-agent-message.ts | 4 +- src/tui/demo.ts | 127 ++- src/tui/description-zone.test.ts | 19 +- src/tui/diff-rows.test.ts | 26 +- src/tui/diff.test.ts | 71 +- src/tui/diff.ts | 73 +- src/tui/dynamic-tool-runner.test.ts | 8 +- src/tui/dynamic-tool-runner.ts | 30 +- src/tui/focus/focus-state.test.ts | 15 +- src/tui/focus/focus-state.ts | 14 +- src/tui/focus/index.ts | 8 +- src/tui/focus/types.ts | 7 +- src/tui/gate-events.ts | 8 +- src/tui/gate-wire.test.ts | 249 ++++-- src/tui/gate-wire.ts | 38 +- src/tui/geometry.test.ts | 86 +- src/tui/geometry/resolve.ts | 79 +- src/tui/gutter-labels.test.ts | 53 +- src/tui/harness.test.ts | 4 +- src/tui/harness.ts | 12 +- src/tui/history-hydrate.test.ts | 37 +- src/tui/history-hydrate.ts | 11 +- src/tui/image-attachments.test.ts | 55 +- src/tui/image-attachments.ts | 70 +- src/tui/keybindings.test.ts | 63 +- src/tui/keybindings.ts | 74 +- src/tui/landing.test.ts | 123 ++- src/tui/landing.ts | 47 +- src/tui/list-modal.ts | 13 +- src/tui/live-session-port.ts | 11 +- src/tui/lockup.test.ts | 28 +- src/tui/log-sink.test.ts | 6 +- src/tui/margins.test.ts | 8 +- src/tui/mark-anim.test.ts | 92 ++- src/tui/mark-anim.ts | 26 +- src/tui/mark-shape.ts | 113 +-- src/tui/markdown-parser.test.ts | 43 +- src/tui/markdown-parser.ts | 171 +++- src/tui/markdown-rows.test.ts | 107 ++- src/tui/mcp-catalog.test.ts | 20 +- src/tui/mcp-copy-failure.test.ts | 13 +- src/tui/mcp-list.test.ts | 27 +- src/tui/mcp-list.ts | 35 +- src/tui/mcp-result-format.ts | 65 +- src/tui/mcp-view.test.ts | 57 +- src/tui/mcp-view.ts | 103 ++- src/tui/mention-filter.test.ts | 11 +- src/tui/mention-popup.test.ts | 27 +- src/tui/mention-resolution.ts | 34 +- src/tui/model-catalog.test.ts | 8 +- src/tui/model-catalog.ts | 61 +- src/tui/notice-line.test.ts | 18 +- src/tui/notice-line.ts | 4 +- src/tui/observe-live.test.ts | 104 ++- src/tui/observe-map.ts | 17 +- src/tui/onboarding.test.ts | 96 ++- src/tui/onboarding.ts | 23 +- src/tui/overlay-body-cache-staleness.test.ts | 4 +- src/tui/overlay-body.test.ts | 48 +- src/tui/overlay-body.ts | 40 +- src/tui/overlay-fixture-fallback.test.ts | 36 +- src/tui/overlay-float-reset.test.ts | 9 +- src/tui/overlay-overflow.test.ts | 51 +- src/tui/overlay-paint.test.ts | 57 +- src/tui/overlay-primary-state.test.ts | 32 +- src/tui/overlay-view.test.ts | 44 +- src/tui/overlay-view.ts | 59 +- src/tui/overlays.test.ts | 124 ++- src/tui/overlays.ts | 40 +- src/tui/palette-paint.test.ts | 52 +- src/tui/pick-session.test.ts | 14 +- src/tui/pick-session.ts | 7 +- src/tui/plugin-diagnostics-sink.test.ts | 58 +- src/tui/plugins-admin-backend.ts | 125 ++- src/tui/product-host.test.ts | 216 +++-- src/tui/product-host.ts | 65 +- src/tui/prompt-attachments.test.ts | 59 +- src/tui/prompt-attachments.ts | 5 +- src/tui/prompt-border.test.ts | 61 +- src/tui/prompt-border.ts | 40 +- src/tui/prompt-box.test.ts | 44 +- src/tui/prompt-chrome.test.ts | 23 +- src/tui/prompt-features.test.ts | 59 +- src/tui/prompt-highlight.test.ts | 21 +- src/tui/prompt-input.ts | 16 +- src/tui/prompt-kill-ring.ts | 13 +- src/tui/prompt-recognition.test.ts | 46 +- src/tui/prompt-recognition.ts | 4 +- src/tui/prompt-rows.ts | 15 +- src/tui/prompt-slash-exit.test.ts | 35 +- src/tui/provider-failure-attempt.test.ts | 12 +- src/tui/provider-setup-submit.test.ts | 189 +++-- src/tui/provider-setup.test.ts | 180 +++-- src/tui/provider/choices.ts | 45 +- src/tui/provider/connect.ts | 25 +- src/tui/provider/discovery.ts | 32 +- src/tui/provider/failure-attempt.ts | 23 +- src/tui/provider/form.ts | 18 +- src/tui/provider/oauth.ts | 85 +- src/tui/provider/setup.ts | 84 +- src/tui/provider/steps.ts | 40 +- src/tui/provider/submit.ts | 40 +- src/tui/provider/surface.ts | 48 +- src/tui/provider/types.ts | 10 +- src/tui/queued-delivery-hop.test.ts | 38 +- src/tui/queued-delivery.ts | 32 +- src/tui/quota-retry.test.ts | 4 +- src/tui/ramp-paint.test.ts | 44 +- src/tui/ramp.test.ts | 34 +- src/tui/ramp.ts | 27 +- src/tui/reasoning-fold.test.ts | 18 +- src/tui/render-loop.test.ts | 12 +- src/tui/request-approval.test.ts | 24 +- src/tui/request-approval.ts | 49 +- src/tui/row-click.test.ts | 5 +- src/tui/row-retext.test.ts | 6 +- src/tui/row-update-perf.test.ts | 50 +- src/tui/row-update-queue.ts | 16 +- src/tui/run-snapshot-kind.test.ts | 31 +- src/tui/runner-exit-code.test.ts | 13 +- src/tui/runner-host.test.ts | 131 +++- src/tui/runner/commands.ts | 75 +- src/tui/runner/exit.test.ts | 8 +- src/tui/runner/exit.ts | 167 +++- src/tui/runner/host.ts | 88 ++- src/tui/runner/index.ts | 40 +- src/tui/runner/mcp.ts | 75 +- src/tui/runner/send-failure-message.ts | 10 +- src/tui/runner/session.ts | 175 +++-- src/tui/runner/settings.ts | 184 +++-- src/tui/runner/shutdown.ts | 4 +- src/tui/runner/state.ts | 96 ++- src/tui/runner/submit.ts | 96 ++- src/tui/runner/wiring.ask-wake.test.ts | 68 +- src/tui/runner/wiring.ts | 96 ++- src/tui/runtime-bridge.test.ts | 381 ++++++--- src/tui/runtime-bridge.ts | 181 ++++- src/tui/runtime-channels.test.ts | 83 +- src/tui/runtime-notices.test.ts | 46 +- src/tui/runtime-notices.ts | 5 +- src/tui/runtime-shutdown.test.ts | 4 +- src/tui/selection-copy.test.ts | 5 +- src/tui/sent-message-history.test.ts | 4 +- src/tui/sent-message-history.ts | 21 +- src/tui/session-chrome.test.ts | 103 ++- src/tui/session-chrome.ts | 23 +- src/tui/session-queue.test.ts | 5 +- src/tui/session-queue.ts | 23 +- src/tui/session-start.test.ts | 6 +- src/tui/session-start.ts | 43 +- src/tui/shell.test.ts | 43 +- src/tui/shell/chrome.ts | 220 ++++-- src/tui/shell/index.ts | 42 +- src/tui/shell/internals.ts | 104 ++- src/tui/shell/keys.ts | 159 +++- src/tui/shell/layout.ts | 14 +- src/tui/shell/observe.ts | 5 +- src/tui/shell/overlay-host.ts | 146 +++- src/tui/shell/overlay-list.ts | 51 +- src/tui/shell/palette.ts | 91 ++- src/tui/shell/prompt.ts | 66 +- src/tui/shell/row-retext.ts | 43 +- src/tui/shell/transcript.ts | 80 +- src/tui/slash-popup-gate.test.ts | 84 +- src/tui/stall-watchdog.test.ts | 71 +- src/tui/stall-watchdog.ts | 17 +- src/tui/steer-worker-invariant.test.ts | 20 +- src/tui/stream-event-map.test.ts | 107 ++- src/tui/stream-event-map.ts | 96 ++- src/tui/stream.test.ts | 149 +++- src/tui/stream.ts | 117 ++- src/tui/submit-handler.test.ts | 42 +- src/tui/syntax-highlight.test.ts | 5 +- src/tui/syntax-highlight.ts | 33 +- src/tui/system-clipboard.test.ts | 9 +- src/tui/system-clipboard.ts | 9 +- src/tui/teardown.test.ts | 12 +- src/tui/thinking-reveal.test.ts | 31 +- src/tui/thinking.ts | 9 +- src/tui/tool-args.ts | 48 +- src/tui/tool-execution-watchdog.test.ts | 180 ++++- src/tui/tool-execution-watchdog.ts | 37 +- src/tui/tool-formatter.test.ts | 177 +++-- src/tui/tool-formatter.ts | 133 +++- src/tui/tool-rows.test.ts | 97 ++- src/tui/tool-rows.ts | 48 +- src/tui/tool-subject.test.ts | 17 +- src/tui/transcript-anchor.test.ts | 5 +- src/tui/transcript-layout.test.ts | 44 +- src/tui/transcript-long-log-scroll.test.ts | 5 +- src/tui/transcript-panels.test.ts | 4 +- src/tui/turn-monitor.test.ts | 72 +- src/tui/turn-state.test.ts | 38 +- src/tui/turn-state.ts | 103 ++- src/tui/turns-to-blocks.test.ts | 46 +- src/tui/turns-to-blocks.ts | 44 +- src/tui/view/height.test.ts | 5 +- src/tui/view/height.ts | 7 +- src/tui/view/lines.ts | 55 +- src/tui/view/spec.ts | 8 +- src/tui/view/validate.ts | 74 +- src/tui/wave6.test.ts | 151 +++- src/tui/wave7.test.ts | 37 +- src/tui/welcome.test.ts | 7 +- src/tui/welcome.ts | 25 +- src/tui/width-columns.test.ts | 13 +- src/tui/width-contract.test.ts | 10 +- src/tui/width-contract.ts | 10 +- src/upgrade/index.test.ts | 9 +- src/upgrade/index.ts | 24 +- src/util/budget-race.ts | 8 +- src/util/control-char-strip.ts | 7 +- src/util/list-dir.ts | 26 +- src/util/tool-output-uri.test.ts | 25 +- src/web/plugin-provider.test.ts | 24 +- src/web/plugin-provider.ts | 18 +- src/web/secret-scrub.test.ts | 4 +- src/web/secret-scrub.ts | 7 +- src/workflows/capabilities.ts | 21 +- src/workflows/coordinator.ts | 6 +- src/workflows/host.ts | 89 ++- src/workflows/index.ts | 6 +- src/workflows/runtime.ts | 18 +- src/workflows/state.ts | 39 +- src/workflows/types.ts | 5 +- tests/fixtures/auth-store-writer.ts | 18 +- tests/fixtures/crash-run/simulate-crash.ts | 21 +- .../crash-run/simulate-exec-signal.ts | 4 +- .../crash-run/simulate-run-end-crash.ts | 5 +- tests/fixtures/crash-run/simulate-signal.ts | 22 +- tests/fixtures/plugins/exa/src/index.test.ts | 26 +- .../plugins/example-agent/src/index.ts | 6 +- .../plugins/example-tool/src/index.ts | 13 +- .../plugins/implement-feature/src/index.ts | 8 +- .../src/workflows/implement-feature.ts | 6 +- tests/fixtures/tier-xhard/src/notify.ts | 9 +- tests/fixtures/tier-xhard/src/store.ts | 11 +- tests/helpers/defined.test.ts | 4 +- tests/helpers/file-log-sink.ts | 4 +- tests/helpers/temporary-git-repo.test.ts | 86 +- tests/helpers/temporary-git-repo.ts | 5 +- tests/helpers/workflows.ts | 21 +- tests/integration/crash-finalize.test.ts | 26 +- tests/integration/exec-shutdown-reap.test.ts | 11 +- .../integration/exec-signal-finalize.test.ts | 11 +- tests/integration/git-push-scoped.test.ts | 45 +- tests/integration/harness.ts | 35 +- tests/integration/mcp-late-dispatch.test.ts | 100 ++- tests/integration/rawmode-sigint.test.ts | 15 +- .../reactor-approval-suspend.test.ts | 300 ++++--- tests/integration/reactor-empty-turn.test.ts | 64 +- .../integration/reactor-events-guards.test.ts | 70 +- .../reactor-permission-multi-turn.test.ts | 97 ++- tests/integration/signal-finalize.test.ts | 15 +- tests/integration/subagent-permission.test.ts | 225 ++++-- tests/integration/vendored-carry.test.ts | 243 +++--- tests/preload.ts | 6 +- tests/unit/agent/tasks.test.ts | 15 +- tests/unit/approval-resume.test.ts | 76 +- tests/unit/check-gate.test.ts | 26 +- tests/unit/codex-auth.test.ts | 78 +- tests/unit/codex-callback-server.test.ts | 18 +- tests/unit/codex-providers.test.ts | 17 +- tests/unit/codex-responses-adapter.test.ts | 250 ++++-- tests/unit/codex-session.test.ts | 57 +- tests/unit/codex-sse-fixtures.test.ts | 33 +- tests/unit/codex-usage.test.ts | 74 +- tests/unit/compactor-pairing.test.ts | 140 +++- tests/unit/config.test.ts | 138 +++- tests/unit/context-window.test.ts | 19 +- tests/unit/corbits-skills-catalog.test.ts | 46 +- tests/unit/data-only-agent.test.ts | 157 +++- tests/unit/data-only-commands.test.ts | 44 +- tests/unit/director.test.ts | 73 +- tests/unit/example-agent-plugin.test.ts | 5 +- tests/unit/exec/runner.test.ts | 139 +++- tests/unit/faremeter.test.ts | 129 ++- tests/unit/generate-homebrew-tap.test.ts | 29 +- tests/unit/grok-responses-adapter.test.ts | 68 +- tests/unit/hooks.test.ts | 46 +- tests/unit/index.test.ts | 13 +- tests/unit/inference-abort.test.ts | 8 +- tests/unit/inference-response-kind.test.ts | 51 +- tests/unit/inference-sources.test.ts | 60 +- tests/unit/mcp-client-unwrap.test.ts | 4 +- tests/unit/mcp-stdio-env.test.ts | 5 +- tests/unit/mcp-tool-name.test.ts | 12 +- tests/unit/mcp-tool-permissions.test.ts | 12 +- tests/unit/mcp.test.ts | 122 ++- tests/unit/openai-responses-adapter.test.ts | 16 +- tests/unit/oxlint-no-bare-mock-module.test.ts | 21 +- tests/unit/path-plugin-trust.test.ts | 40 +- tests/unit/path-trust.test.ts | 80 +- .../cross-commit-composition.test.ts | 13 +- tests/unit/plugin-loader-path.test.ts | 39 +- tests/unit/plugin-marketplace.test.ts | 56 +- tests/unit/plugin-register.test.ts | 115 ++- tests/unit/plugin-repo-locator.test.ts | 19 +- .../unit/prepare-homebrew-tap-release.test.ts | 24 +- tests/unit/pricing-fetcher.test.ts | 11 +- tests/unit/project-trust.test.ts | 61 +- tests/unit/resolve-inference-spec.test.ts | 26 +- tests/unit/ripgrep-plugin.test.ts | 41 +- tests/unit/run-agent.test.ts | 4 +- .../unit/session/run-sink-exec-status.test.ts | 5 +- tests/unit/session/run-state-e2e.test.ts | 46 +- tests/unit/skill-commands.test.ts | 42 +- tests/unit/skills.test.ts | 86 +- tests/unit/subagent-session-store.test.ts | 87 ++- tests/unit/summarizer.test.ts | 21 +- tests/unit/telemetry-first-run.test.ts | 22 +- tests/unit/telemetry-product-events.test.ts | 150 +++- tests/unit/telemetry-toggle.test.ts | 36 +- tests/unit/telemetry.test.ts | 78 +- tests/unit/tui/agent-source-sync.test.ts | 8 +- tests/unit/tui/agent-tools.test.ts | 188 +++-- .../approval-reload-during-suspend.test.ts | 18 +- tests/unit/tui/at-mention-resolution.test.ts | 79 +- tests/unit/tui/mcp-result-format.test.ts | 40 +- tests/unit/tui/onboarded-persistence.test.ts | 12 +- tests/unit/tui/run-sink.test.ts | 10 +- tests/unit/tui/runner.test.ts | 51 +- tests/unit/tui/theme.test.ts | 17 +- .../unit/tui/tool-formatter-web-brand.test.ts | 5 +- tests/unit/tui/view-render.test.ts | 6 +- tests/unit/vendor-patch-ledger.test.ts | 6 +- tests/unit/verify-corbits-only-scope.test.ts | 13 +- tests/unit/workflow-host.test.ts | 27 +- tests/unit/workflows-capabilities.test.ts | 32 +- tests/unit/workflows-definitions.test.ts | 11 +- tests/unit/workflows-director.test.ts | 120 ++- tests/unit/workflows-registry.test.ts | 5 +- .../workflows-runtime-persistence.test.ts | 5 +- tests/unit/workflows-runtime.test.ts | 68 +- tests/unit/workflows-state.test.ts | 43 +- tests/unit/xai-usage.test.ts | 11 +- tsconfig.json | 4 +- 830 files changed, 33516 insertions(+), 10294 deletions(-) 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 index 9f5512e9d..996087fa0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -42,7 +42,10 @@ export default tseslint.config( ], // 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 }], + "@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; @@ -70,7 +73,8 @@ export default tseslint.config( "no-restricted-syntax": [ "error", { - selector: "CallExpression[callee.object.name='mock'][callee.property.name='module']", + 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 b9cdcb396..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", @@ -192,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) @@ -206,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, ); } @@ -216,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; @@ -240,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); @@ -250,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 cb6004f08..ebae5a57b 100644 --- a/evals/capability/lib.test.ts +++ b/evals/capability/lib.test.ts @@ -191,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", () => { @@ -235,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", ), @@ -245,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([]); }); @@ -281,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); }); @@ -293,14 +308,24 @@ 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", () => { @@ -318,12 +343,18 @@ describe("parseMatrix", () => { 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", () => { @@ -338,7 +369,12 @@ 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", () => { @@ -380,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); @@ -426,7 +473,11 @@ describe("computeCellAggregates", () => { 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", () => { @@ -436,8 +487,14 @@ describe("computeCellAggregates", () => { 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(defined(cells[0]).behaviorStats.shellCommandCount?.median).toBe(3); }); @@ -451,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 }), ]; @@ -536,7 +617,9 @@ describe("compareToBaseline", () => { ]; const cmp = compareToBaseline(current, cleanBaseline, [baitCase]); expect(cmp.baitFlags).toBe(1); - expect(defined(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", () => { @@ -672,8 +755,12 @@ describe("resolveRequestedProviderModel", () => { expect(requested).toEqual({ provider: raw.provider, model: raw.model }); const fallback = 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: defined(cell).provider, resolvedModel: defined(cell).model, }); @@ -710,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/, @@ -725,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 = defined(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(); }); }); @@ -750,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(defined(report.cases[0]).behaviors?.shellCommandCount).toBe(2); expect(defined(report.cases[1]).repeat).toBe(1); expect(report.aggregates).toHaveLength(1); - expect(defined(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", () => { diff --git a/evals/capability/lib.ts b/evals/capability/lib.ts index 056049764..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 } : {}), }, @@ -619,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 } : {}) }; @@ -639,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, @@ -660,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, @@ -676,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"), @@ -690,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 @@ -703,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 { @@ -715,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, }; } @@ -753,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, }; @@ -790,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); @@ -802,7 +884,8 @@ export function computeCellAggregates(results: readonly CaseResult[]): CellAggre 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]) @@ -831,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 } : {}), @@ -849,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 { @@ -878,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]; @@ -900,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/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 892d0c349..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,7 +47,10 @@ 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)); + const index = Math.min( + sorted.length - 1, + Math.floor((p / 100) * sorted.length), + ); const value = sorted[index]; if (value === undefined) return 0; return value; @@ -105,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 @@ -122,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); @@ -161,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 fa2a74f84..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, @@ -178,8 +189,12 @@ 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]; @@ -209,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; @@ -244,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; } @@ -308,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, @@ -355,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([ @@ -413,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) { @@ -457,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 = [ @@ -474,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)); @@ -529,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, @@ -549,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"); @@ -621,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 ?? @@ -718,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"); @@ -764,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}`); } @@ -794,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) { @@ -805,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) { @@ -835,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 { @@ -869,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); @@ -927,20 +1010,25 @@ 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(); @@ -1001,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 8953b0d2f..d08bff741 100644 --- a/scripts/eval-public-swe-one.ts +++ b/scripts/eval-public-swe-one.ts @@ -171,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) => { @@ -214,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} …`); @@ -241,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 }); @@ -290,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 { @@ -332,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)})`, ); @@ -437,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 f3da6e37e..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", diff --git a/scripts/intervention-forensics.ts b/scripts/intervention-forensics.ts index 2445d45cd..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,7 +63,10 @@ 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)); + const index = Math.min( + sorted.length - 1, + Math.floor((p / 100) * sorted.length), + ); const value = sorted[index]; if (value === undefined) return 0; return value; @@ -129,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++; @@ -148,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); @@ -164,14 +176,18 @@ 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]; @@ -179,7 +195,8 @@ for (const [key, bucket] of rows) { sorted.length === 0 || last === undefined ? "-" : `${percentile(sorted, 50)}/${percentile(sorted, 90)}/${last}`; - const thresholds = bucket.thresholds.size === 0 ? "-" : [...bucket.thresholds].join(","); + 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)}`, ); @@ -208,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}`); @@ -231,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 index 4339367a3..bf9e0b273 100644 --- a/scripts/oxlint-plugin-corbits.js +++ b/scripts/oxlint-plugin-corbits.js @@ -16,10 +16,16 @@ const noBareMockModule = { const callee = node.callee; if (callee.type !== "MemberExpression") return; if (callee.computed) return; - if (callee.object.type !== "Identifier" || callee.object.name !== "mock") { + if ( + callee.object.type !== "Identifier" || + callee.object.name !== "mock" + ) { return; } - if (callee.property.type !== "Identifier" || callee.property.name !== "module") { + if ( + callee.property.type !== "Identifier" || + callee.property.name !== "module" + ) { return; } context.report({ node, messageId: "noBare" }); 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 87ef2dce7..2782cc283 100644 --- a/src/agent/agent-search.test.ts +++ b/src/agent/agent-search.test.ts @@ -12,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", @@ -80,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:"); @@ -101,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", () => { @@ -140,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); @@ -155,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"); @@ -166,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."); }); @@ -183,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 30a298cd2..025a52b46 100644 --- a/src/agent/background-shell-tool.test.ts +++ b/src/agent/background-shell-tool.test.ts @@ -37,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", @@ -46,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, ); @@ -66,7 +74,9 @@ describe("background shell through the agent toolset", () => { expect(exits).toHaveLength(1); 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.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"); @@ -95,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", @@ -104,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 d7c1abb11..b1af9325e 100644 --- a/src/agent/codex-apply-patch.test.ts +++ b/src/agent/codex-apply-patch.test.ts @@ -29,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", () => { @@ -158,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", () => { diff --git a/src/agent/codex-apply-patch.ts b/src/agent/codex-apply-patch.ts index 1b704d46b..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); @@ -121,25 +125,37 @@ export function parseCodexApplyPatch(input: string): ParsedPatch { ); } // 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; const maybeMove = body[i]; if (maybeMove !== undefined && maybeMove.startsWith(MOVE_TO)) { - moveTo = requireRelativePath(maybeMove.slice(MOVE_TO.length), "Move to"); + moveTo = requireRelativePath( + maybeMove.slice(MOVE_TO.length), + "Move to", + ); i += 1; } const hunks: PatchHunk[] = []; @@ -211,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; } @@ -233,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; } @@ -256,7 +283,10 @@ export function contentFromAddOp(op: PatchAddOp): string { return op.content; } -function parseHunk(body: string[], start: number): { hunk: PatchHunk; next: number } { +function parseHunk( + body: string[], + start: number, +): { hunk: PatchHunk; next: number } { const headerLine = body[start]; if (headerLine === undefined) { throw new CodexApplyPatchError("expected hunk start '@@'"); @@ -269,7 +299,9 @@ 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; @@ -281,7 +313,9 @@ function parseHunk(body: string[], start: number): { hunk: PatchHunk; next: numb i += 1; return { hunk: - header === undefined ? { lines, endOfFile: true } : { header, lines, endOfFile: true }, + header === undefined + ? { lines, endOfFile: true } + : { header, lines, endOfFile: true }, next: i, }; } @@ -316,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 { @@ -375,9 +413,14 @@ 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++) { @@ -394,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 2e4e0f08a..525fc0d7f 100644 --- a/src/agent/codex-tool-mount.test.ts +++ b/src/agent/codex-tool-mount.test.ts @@ -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 7e818b1de..f913350f6 100644 --- a/src/agent/codex-tool-proxies.test.ts +++ b/src/agent/codex-tool-proxies.test.ts @@ -67,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 @@ -104,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", () => { @@ -127,7 +136,11 @@ 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(defined(tools[0]).definition.inputSchema).toMatchObject({ required: ["input"], @@ -163,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, @@ -178,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, @@ -210,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, @@ -240,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, @@ -270,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, @@ -300,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, @@ -356,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); @@ -443,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 () => { @@ -456,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 () => { @@ -473,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 }, + }, ]); }); @@ -507,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, @@ -599,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 7c0905b0c..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"], @@ -364,18 +384,27 @@ function normalizeShellCommand(command: string | string[]): string { 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"); @@ -422,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"; @@ -434,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 @@ -446,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 541a85675..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); @@ -135,10 +162,14 @@ describe("compaction governor", () => { test("stays inert below the threshold or with few turns", () => { 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", () => { @@ -146,25 +177,39 @@ describe("compaction governor", () => { 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(() => undefined); - expect(governor.interceptOverflow(overflowError(), capabilities)).not.toBeNull(); + 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,7 +317,9 @@ 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", () => { @@ -259,7 +328,11 @@ describe("compaction governor", () => { 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); }); @@ -270,13 +343,20 @@ describe("compaction governor", () => { 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(() => undefined); - governor.noteInferenceDone(inferenceDoneWithoutUsage(), turnsOfLength(10, 4)); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + 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", () => { @@ -288,7 +368,11 @@ describe("compaction governor", () => { 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); }); @@ -298,11 +382,15 @@ describe("compaction governor", () => { // 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", () => { @@ -322,10 +410,18 @@ describe("compaction governor", () => { 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(); }); @@ -334,8 +430,13 @@ describe("compaction governor", () => { // 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(() => undefined); - governor.noteInferenceDone(inferenceDone(overThreshold * 10), turnsOfLength(2, 1)); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + 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", () => { @@ -345,12 +446,20 @@ describe("compaction governor", () => { // inference.done ever runs. 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); }); @@ -361,15 +470,27 @@ describe("compaction governor", () => { // would spend a reactor cycle that is guaranteed to shrink nothing. const floor = compactorNoOpFloor(COMPACTOR_KEEP_RECENT_TURNS); const governor = createCompactionGovernor(() => undefined); - governor.noteInferenceDone(inferenceDone(overThreshold), turnsOfLength(floor, 1)); - expect(governor.interceptActions(toolDone(), inferAction, capabilities)).toBeNull(); + 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(() => undefined); - governor.noteInferenceDone(inferenceDone(overThreshold), turnsOfLength(floor + 1, 1)); - const actions = governor.interceptActions(toolDone(), inferAction, capabilities); + 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); }); @@ -384,14 +505,20 @@ describe("compaction governor", () => { // usage-omitted case covered above. 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", () => { @@ -416,24 +543,39 @@ describe("compaction governor", () => { test("does not re-arm after a compact that remains over the high watermark", () => { 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(() => 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); }); @@ -441,17 +583,27 @@ describe("compaction governor", () => { test("clears hysteresis once usage drops under the high watermark", () => { 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); }); @@ -459,10 +611,14 @@ describe("compaction governor", () => { test("overflow still compact while hysteresis blocks the proactive path", () => { 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(); @@ -473,43 +629,74 @@ describe("compaction governor", () => { 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 03c76084c..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; } @@ -102,7 +125,9 @@ describe("ChatDirector tool-only loop protection", () => { 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(); @@ -119,9 +144,11 @@ describe("ChatDirector tool-only loop protection", () => { 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); }); }); @@ -159,7 +186,11 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { 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 @@ -175,18 +206,26 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { 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); }); @@ -214,16 +253,24 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { 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), ); @@ -250,11 +297,15 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { 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; } @@ -269,11 +320,19 @@ describe("ChatDirector inference-error recovery (CL-6910)", () => { 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 076370a41..bbf0699b2 100644 --- a/src/agent/retry-policy.test.ts +++ b/src/agent/retry-policy.test.ts @@ -1,6 +1,9 @@ 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`; @@ -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" }); }); @@ -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 8cf8cbc3e..3ea859b31 100644 --- a/src/agent/tool-schema-normalize.test.ts +++ b/src/agent/tool-schema-normalize.test.ts @@ -40,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); }); @@ -69,7 +72,9 @@ 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(defined(present).description).toBe(recursivePresent.description); @@ -89,7 +94,9 @@ describe("normalizeToolDefinitionsForProvider", () => { 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)", () => { @@ -107,7 +114,9 @@ describe("normalizeToolDefinitionsForProvider", () => { providerName: "openai-compat", model: "kimi-k3", }); - expect(schemaHasRef(defined(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)", () => { 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 60af74922..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)."; @@ -569,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) { @@ -577,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") { @@ -595,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() ?? [], @@ -604,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), }), ); @@ -620,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; @@ -648,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( @@ -691,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); }; @@ -717,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 => { @@ -728,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); } } @@ -752,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") { @@ -816,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") { @@ -1022,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(); @@ -1036,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( + "