From 50304720309e37e2b795dc6ed543c77b64cf298f Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Mon, 31 Aug 2026 11:12:18 -0300 Subject: [PATCH 1/7] feat: support OpenCode v2 plugin API - preserve OpenCode 1 Navigator and Rift behavior - share tool definitions across plugin versions - document version-specific capabilities --- README.md | 16 +- bun.lock | 163 +++++- packages/opencode/README.md | 16 +- packages/opencode/config.ts | 2 +- packages/opencode/index.ts | 485 +----------------- packages/opencode/legacy-plugin.ts | 63 +++ packages/opencode/logging.ts | 22 +- packages/opencode/navigator.ts | 3 +- packages/opencode/package.json | 11 +- packages/opencode/plugin.ts | 8 +- packages/opencode/scripts/build.ts | 1 - packages/opencode/test/agents-config.test.ts | 1 - packages/opencode/test/plugin-entry.test.ts | 14 +- .../opencode/test/tool-registration.test.ts | 4 +- packages/opencode/test/v2-plugin.test.ts | 103 ++++ packages/opencode/v1.ts | 478 +++++++++++++++++ packages/opencode/v2.ts | 163 ++++++ .../content/docs/docs/adapters/opencode.mdx | 8 +- .../src/content/docs/docs/config/schema.mdx | 2 +- .../docs/docs/reference/tools/index.mdx | 2 +- .../docs/docs/reference/tools/navigator.mdx | 2 +- 21 files changed, 1044 insertions(+), 523 deletions(-) create mode 100644 packages/opencode/legacy-plugin.ts create mode 100644 packages/opencode/test/v2-plugin.test.ts create mode 100644 packages/opencode/v1.ts create mode 100644 packages/opencode/v2.ts diff --git a/README.md b/README.md index 60e5f99..68ceb73 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl - Commands cover direct work (`/ask`, `/commit`, `/merge`, `/skill/create`, `/skill/optimize`), orchestration (`/dev`, `/ship`, `/todo`, `/pr/fix/loop`), ticket planning/sync, and PR review/shipping flows. `/branch/inline`, `/commit/inline`, `/commit-and-push/inline`, `/pr/create/inline`, and `/ship/inline` reuse the invoking session instead of starting a subtask. - Agents are intentionally narrow: `worker` handles implementation and multi-step workflows, `planner` is no-edit planning, and `reviewer` is a no-edit review specialist. -- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. OpenCode users can opt into the eight Navigator session and worktree tools. +- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. OpenCode 1 also includes the eight Navigator session and worktree tools. - Reusable command-template components live in `packages/core/components/` and are documented in the components reference. ## Prerequisites @@ -29,7 +29,7 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl ## Installation -For OpenCode, add the adapter package to your config: +The same package supports both OpenCode plugin APIs. For OpenCode 1, add: ```json { @@ -37,6 +37,16 @@ For OpenCode, add the adapter package to your config: } ``` +For OpenCode 2, use the plural `plugins` key: + +```json +{ + "plugins": ["@kompassdev/opencode"] +} +``` + +OpenCode 2 agent transforms are update-only. If an agent is renamed in Kompass config, the host must register that agent ID before Kompass setup runs. + Project config is optional. To start from the published base config: ```bash @@ -54,7 +64,7 @@ The recommended project override path is `.opencode/kompass.jsonc`. ## Kompass Navigator -Navigator is an OpenCode capability for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. It is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. Navigator is enabled by default, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer. +Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required to register Navigator safely. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer. Configure Navigator and its limits with: diff --git a/bun.lock b/bun.lock index bfa0525..ecb659a 100644 --- a/bun.lock +++ b/bun.lock @@ -18,19 +18,16 @@ }, "packages/opencode": { "name": "@kompassdev/opencode", - "version": "0.16.1", + "version": "0.17.4", "dependencies": { + "@opencode-ai/plugin": "0.0.0-beta-18684", + "@opencode-ai/sdk": "^1.18.7", "rift-snapshot": "^0.0.10", + "zod": "^4.1.8", }, "devDependencies": { - "@opencode-ai/plugin": "^1.18.7", - "@opencode-ai/sdk": "^1.18.7", "yaml": "^2.8.2", }, - "peerDependencies": { - "@opencode-ai/plugin": "^1.18.7", - "@opencode-ai/sdk": "^1.18.7", - }, }, "packages/web": { "name": "@kompassdev/web", @@ -70,6 +67,12 @@ "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.3", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -210,6 +213,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@kompassdev/core": ["@kompassdev/core@workspace:packages/core"], @@ -232,7 +237,15 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@opencode-ai/plugin": ["@opencode-ai/plugin@1.18.7", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.18.7", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-kXm5hqur5JiF/xJPaQYkuNn7vSl4ya09YgL/pIvIrggWCLFsfo1gXqNbONRcGW7m5Ih2k6kgMXFyjB9or1+dEg=="], + "@opencode-ai/ai": ["@opencode-ai/ai@0.0.0-beta-18684", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-beta-18684", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-gZTPELkm9aSv0cxWt9gGSdN9fecO9/GlplzUT4knrBD70vxQGnOYx7LGDUPZokW8Fzn7gjD824Qh9AIVjyb07g=="], + + "@opencode-ai/client": ["@opencode-ai/client@0.0.0-beta-18684", "", { "dependencies": { "@opencode-ai/protocol": "0.0.0-beta-18684", "@opencode-ai/schema": "0.0.0-beta-18684" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-Qjq07sTId3gZcqqMsPg+gtqU3PXneOWje+BSzBXepimAfPNqL0aIpH+em2Y4KL3lxEuJK3vdfyx8UXI8gzGRlQ=="], + + "@opencode-ai/plugin": ["@opencode-ai/plugin@0.0.0-beta-18684", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/ai": "0.0.0-beta-18684", "@opencode-ai/client": "0.0.0-beta-18684", "@opencode-ai/protocol": "0.0.0-beta-18684", "@opencode-ai/schema": "0.0.0-beta-18684", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode-ai/theme": "0.0.0-beta-18684", "@opentui/core": ">=0.5.9", "@opentui/solid": ">=0.5.9", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode-ai/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-OOgIRF563MsMROHrxXuJtdG5729PE1DhN4ASRW6OuH0KNjEbIkCrnOJmBI5by+JSbwTbHrsgKXhk18ee9MpRUg=="], + + "@opencode-ai/protocol": ["@opencode-ai/protocol@0.0.0-beta-18684", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-beta-18684", "effect": "4.0.0-rc.112" } }, "sha512-BXeauVOxxcam0zgOxJcpkmoQQTiqq3L6l3i8qF/Wsa3iPf7cA1SCM6+3r6iJ/9akckEEi9ZL85M3b8NhOcU85A=="], + + "@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-beta-18684", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-aoQEG+KBSTAq7IaNVpAfXKNSvcBaRwpjPgPL0DuKoj39g013/NoR/rmbWtjxKkkczBqbWp/91zLGvXW+bIOotQ=="], "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.7", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-5txVELvCxzC3O/MYnRKdTvBN3TqQjE3PaEks3kDRA/moWzMSUhdm15xCth/Mz/NPYyGTn2u4IdScdV9vLTfLRg=="], @@ -252,6 +265,8 @@ "@pagefind/windows-x64": ["@pagefind/windows-x64@1.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="], @@ -318,6 +333,20 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-nxu3SgmAw9JXT2CtkU0m/XNLWpP9MsaBx1zAGAypCbYj15tIFlmcYwpF+Oh18le83d+IM9PT7ENdXnE4C+d5mA=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-iq+cW3mAb7vfcxEEpYi3zXKpDtbrIFyanWjQl4zBq4seWD4OSxXDWSfespZxenX6aEaighn+NR3u1nU1DSvs3w=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], @@ -366,6 +395,8 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -392,20 +423,32 @@ "astro-expressive-code": ["astro-expressive-code@0.38.3", "", { "dependencies": { "rehype-expressive-code": "^0.38.3" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-Tvdc7RV0G92BbtyEOsfJtXU35w41CkM94fOAzxbQP67Wj5jArfserJ321FO4XA7WG9QMV0GIBmQq77NBIRDzpQ=="], + "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], + "brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -462,6 +505,8 @@ "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -496,7 +541,11 @@ "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], - "effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="], "emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="], @@ -540,7 +589,7 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], + "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -548,7 +597,7 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], @@ -556,14 +605,30 @@ "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="], + + "gcp-metadata": ["gcp-metadata@8.1.4", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], + + "google-logging-utils": ["google-logging-utils@1.2.0", "", {}, "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A=="], + + "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], + "h3": ["h3@1.15.10", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg=="], "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], @@ -614,12 +679,12 @@ "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], @@ -644,17 +709,23 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], @@ -778,24 +849,30 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.0.3", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-vFKpMYFTEQujRQxvdS/u6zlfesws0J40K74w6E1fVsYnIa9WKJKB5xIVVON8L7S39hCNrCVGXcPjrYmCb9lT+w=="], + "msgpackr": ["msgpackr@2.1.0", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ=="], "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], - "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], @@ -820,6 +897,8 @@ "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="], @@ -834,6 +913,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -918,8 +999,12 @@ "rift-snapshot": ["rift-snapshot@0.0.10", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "rift": "bin/rift.js" } }, "sha512-A10cjyHqylhvD+Qu7gu/oCZ7z7euhsDV1K3Uz0byATct2Vt6IYvGDbuRcE/Hq7RrqBOGM8STLeFm8EPpTBYKcw=="], + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -932,6 +1017,8 @@ "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="], @@ -948,10 +1035,14 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -964,8 +1055,6 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -1018,8 +1107,6 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], @@ -1066,6 +1153,8 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], @@ -1074,6 +1163,8 @@ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -1092,7 +1183,7 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], @@ -1100,12 +1191,20 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@astrojs/sitemap/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@astrojs/yaml2ts/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@expressive-code/plugin-shiki/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1120,8 +1219,14 @@ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "sitemap/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], @@ -1138,12 +1243,16 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="], @@ -1156,6 +1265,10 @@ "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], diff --git a/packages/opencode/README.md b/packages/opencode/README.md index 60e5f99..68ceb73 100644 --- a/packages/opencode/README.md +++ b/packages/opencode/README.md @@ -20,7 +20,7 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl - Commands cover direct work (`/ask`, `/commit`, `/merge`, `/skill/create`, `/skill/optimize`), orchestration (`/dev`, `/ship`, `/todo`, `/pr/fix/loop`), ticket planning/sync, and PR review/shipping flows. `/branch/inline`, `/commit/inline`, `/commit-and-push/inline`, `/pr/create/inline`, and `/ship/inline` reuse the invoking session instead of starting a subtask. - Agents are intentionally narrow: `worker` handles implementation and multi-step workflows, `planner` is no-edit planning, and `reviewer` is a no-edit review specialist. -- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. OpenCode users can opt into the eight Navigator session and worktree tools. +- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. OpenCode 1 also includes the eight Navigator session and worktree tools. - Reusable command-template components live in `packages/core/components/` and are documented in the components reference. ## Prerequisites @@ -29,7 +29,7 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl ## Installation -For OpenCode, add the adapter package to your config: +The same package supports both OpenCode plugin APIs. For OpenCode 1, add: ```json { @@ -37,6 +37,16 @@ For OpenCode, add the adapter package to your config: } ``` +For OpenCode 2, use the plural `plugins` key: + +```json +{ + "plugins": ["@kompassdev/opencode"] +} +``` + +OpenCode 2 agent transforms are update-only. If an agent is renamed in Kompass config, the host must register that agent ID before Kompass setup runs. + Project config is optional. To start from the published base config: ```bash @@ -54,7 +64,7 @@ The recommended project override path is `.opencode/kompass.jsonc`. ## Kompass Navigator -Navigator is an OpenCode capability for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. It is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. Navigator is enabled by default, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer. +Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required to register Navigator safely. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer. Configure Navigator and its limits with: diff --git a/packages/opencode/config.ts b/packages/opencode/config.ts index 26bab67..a3b00e1 100644 --- a/packages/opencode/config.ts +++ b/packages/opencode/config.ts @@ -1,5 +1,5 @@ -import type { Config } from "@opencode-ai/plugin"; import type { AgentConfig } from "@opencode-ai/sdk"; +import type { Config } from "./legacy-plugin.ts"; import { loadResolvedAgents, loadResolvedCommands } from "./cache.ts"; import type { PluginLogger } from "./logging.ts"; diff --git a/packages/opencode/index.ts b/packages/opencode/index.ts index 03c1818..8fcdf10 100644 --- a/packages/opencode/index.ts +++ b/packages/opencode/index.ts @@ -1,471 +1,24 @@ -import type { Hooks, Plugin, PluginInput } from "@opencode-ai/plugin"; -import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"; -import { createOpencodeClient as createLegacyClient, type OpencodeClient as LegacyClient } from "@opencode-ai/sdk/client"; -import { createOpencodeClient as createV2Client, type OpencodeClient as V2Client } from "@opencode-ai/sdk/v2"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; - -import { - createChangesLoadTool, - createPrLoadTool, - createPrLoadReviewTool, - createPrSyncTool, - createTicketLoadTool, - createTicketSyncTool, - getEnabledToolNames, - type MergedKompassConfig, - type Shell, - type ShellPromise, -} from "../core/index.ts"; -import { loadMergedKompassConfig } from "./cache.ts"; -import { applyAgentsConfig, applyCommandsConfig } from "./config.ts"; -import { createPluginLogger, getErrorDetails, type PluginLogger } from "./logging.ts"; -import { createNavigatorTools, detectNavigatorProtocol, getNavigatorCompatibilityWarning } from "./navigator.ts"; -import { registerRiftWorkspaceAdapter } from "./rift-workspace.ts"; +import { OpenCodeCompassPluginV1 } from "./v1.ts"; import { - getConfiguredOpenCodeToolName, -} from "./tool-names.ts"; - -const execFileAsync = promisify(execFile); - -type CommandExecuteBeforeHook = NonNullable; -type CommandExecuteBeforeInput = Parameters[0]; -type CommandExecuteBeforeOutput = Parameters[1]; -type OpenCodeToolCreator = ( - client: PluginInput["client"], - config: MergedKompassConfig, - projectRoot: string, - shell: Shell, -) => ToolDefinition; - -type ShellResult = ShellPromise & { - stdout: Buffer; -}; - -function shellEscape(value: unknown): string { - return `'${String(value).replaceAll("'", `'\\''`)}'`; -} - -function shellResult(stdout: string, stderr: string, exitCode: number): ShellResult { - return { - cwd: () => { - throw new Error("Cannot change cwd after execution"); - }, - quiet: () => { - throw new Error("Cannot change quiet after execution"); - }, - nothrow: () => { - throw new Error("Cannot change nothrow after execution"); - }, - text: () => stdout, - json: () => JSON.parse(stdout), - exitCode, - stderr: Buffer.from(stderr), - stdout: Buffer.from(stdout), - }; -} - -class NodeShellCommand implements PromiseLike { - #command: string; - #cwd: string; - #quiet = false; - #nothrow = false; - #result?: Promise; - - constructor(command: string, cwd: string) { - this.#command = command; - this.#cwd = cwd; - } - - cwd(dir: string) { - this.#cwd = dir; - return this; - } - - quiet() { - this.#quiet = true; - return this; - } - - nothrow() { - this.#nothrow = true; - return this; - } - - then( - onfulfilled?: ((value: ShellResult) => TResult1 | PromiseLike) | null, - onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, - ) { - return this.run().then(onfulfilled, onrejected); - } - - private run() { - this.#result ??= this.execute(); - return this.#result; - } - - private async execute() { - try { - const { stdout, stderr } = await execFileAsync("/bin/bash", ["-lc", this.#command], { - cwd: this.#cwd, - encoding: "utf8", - maxBuffer: 10 * 1024 * 1024, - }); - - this.write(stdout, stderr); - return shellResult(stdout, stderr, 0); - } catch (error) { - const failed = error as { stdout?: unknown; stderr?: unknown; message?: unknown; code?: unknown }; - const stdout = String(failed.stdout ?? ""); - const stderr = String(failed.stderr ?? failed.message ?? ""); - const exitCode = typeof failed.code === "number" ? failed.code : 1; - const result = shellResult(stdout, stderr, exitCode); - - this.write(stdout, stderr); - if (this.#nothrow) return result; - - throw new Error(stderr || `Command failed: ${this.#command}`); - } - } - - private write(stdout: string, stderr: string) { - if (this.#quiet) return; - if (stdout) process.stdout.write(stdout); - if (stderr) process.stderr.write(stderr); - } -} - -function createNodeShell(defaultDirectory: string): Shell { - return (strings: TemplateStringsArray, ...expressions: unknown[]) => { - let command = strings[0] ?? ""; - - expressions.forEach((expression, index) => { - command += Array.isArray(expression) - ? expression.map((item) => shellEscape(item)).join(" ") - : shellEscape(expression); - command += strings[index + 1] ?? ""; - }); - - return new NodeShellCommand(command, defaultDirectory) as unknown as ShellPromise; - }; -} - -export type CommandExecution = { - command: string; - arguments: string; - prompt: string; + OpenCodeCompassPluginV2, + setupOpenCodeV2, +} from "./v2.ts"; + +export const OpenCodeCompassPlugin = { + id: "kompass", + server: OpenCodeCompassPluginV1, + setup: setupOpenCodeV2, }; -function getString(value: unknown): string | undefined { - return typeof value === "string" ? value : undefined; -} - -function logObservedFailure( - logger: PluginLogger, - message: string, - error: unknown, - extra?: Record, -) { - logger.warn(message, { - ...(extra ?? {}), - ...getErrorDetails(error), - }); -} - -export function getCommandExecution( - input: CommandExecuteBeforeInput, - output: CommandExecuteBeforeOutput, -): CommandExecution | undefined { - const prompt = output.parts - .flatMap((part) => part.type === "text" ? [part.text] : []) - .join("\n") - .trim(); - - if (!prompt) return; - - return { - command: input.command, - arguments: input.arguments, - prompt, - }; -} - -const opencodeToolCreators: Record = { - changes_load(_: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string, shell: Shell) { - const definition = createChangesLoadTool(shell); - return tool({ - description: definition.description, - args: { - base: tool.schema.string().describe("Base branch or ref").optional(), - head: tool.schema.string().describe("Head branch, commit, or ref override").optional(), - depthHint: tool.schema.number().int().positive() - .describe("Optional shallow-fetch hint, such as PR commit count") - .optional(), - uncommitted: tool.schema.boolean() - .describe("Only load uncommitted changes (staged and unstaged), never fall back to branch comparison") - .optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - pr_load(_: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string, shell: Shell) { - const definition = createPrLoadTool(shell); - return tool({ - description: definition.description, - args: { - pr: tool.schema.string().describe("PR number or URL").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - pr_load_review(_: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string, shell: Shell) { - const definition = createPrLoadReviewTool(shell); - return tool({ - description: definition.description, - args: { - pr: tool.schema.string().describe("PR number or URL").optional(), - since: tool.schema.string().describe("Exclusive ISO-8601 review checkpoint"), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - pr_sync(_: PluginInput["client"], config: MergedKompassConfig, _projectRoot: string, shell: Shell) { - const definition = createPrSyncTool(shell); - - return tool({ - description: definition.description, - args: { - title: tool.schema.string().describe("PR title; required when creating or renaming a PR").optional(), - body: tool.schema.string().describe("PR body override").optional(), - description: tool.schema.string().describe("Short PR description rendered above checklist sections").optional(), - base: tool.schema.string().describe("Base branch to merge into").optional(), - head: tool.schema.string().describe("Head branch to use when creating a PR").optional(), - labels: tool.schema.array(tool.schema.string()).describe("Labels to add to the PR").optional(), - removeLabels: tool.schema.array(tool.schema.string()).describe("Labels to remove from an existing PR").optional(), - replaceLabels: tool.schema.array(tool.schema.string()).describe("Exact label set for the PR; an empty array clears all labels").optional(), - assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the PR").optional(), - checklists: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist section name"), - items: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist item name"), - completed: tool.schema.boolean().describe("Whether the item is completed"), - })).describe("Checklist items"), - })).describe("Checklist sections rendered as markdown").optional(), - draft: tool.schema.boolean().describe("Create as draft PR").optional(), - refUrl: tool.schema.string().describe("Optional PR URL to update").optional(), - commitId: tool.schema.string().describe("Commit SHA to anchor review comments to; omit unless sending review comments").optional(), - review: tool.schema.object({ - body: tool.schema.string().describe("Optional review summary body").optional(), - comments: tool.schema.array(tool.schema.object({ - path: tool.schema.string().describe("Changed file path"), - body: tool.schema.string().describe("Inline review comment body"), - line: tool.schema.number().int().describe("Ending line on the diff side"), - startLine: tool.schema.number().int().describe("Starting line for multi-line comments").optional(), - side: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the ending line").optional(), - startSide: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the starting line").optional(), - })).describe("Inline review comments to submit").optional(), - ...(config.shared.prApprove - ? { approve: tool.schema.boolean().describe("Approve the PR with this review comment").optional() } - : {} - ), - }).describe("Optional structured review submission; omit the field entirely unless submitting a review body, inline comments, or approval").optional(), - replies: tool.schema.array(tool.schema.object({ - inReplyTo: tool.schema.number().int().describe("Existing review comment ID to reply to"), - body: tool.schema.string().describe("Reply body"), - })).describe("Replies to existing review comments").optional(), - commentBody: tool.schema.string().describe("General PR comment body").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - ticket_sync(_: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string, shell: Shell) { - const definition = createTicketSyncTool(shell); - return tool({ - description: definition.description, - args: { - title: tool.schema.string().describe("Issue title").optional(), - body: tool.schema.string().describe("Issue body override").optional(), - description: tool.schema.string().describe("Issue description rendered above checklist sections").optional(), - labels: tool.schema.array(tool.schema.string()).describe("Labels to apply to the issue").optional(), - assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the issue").optional(), - checklists: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist section name"), - items: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist item name"), - completed: tool.schema.boolean().describe("Whether the item is completed"), - })).describe("Checklist items"), - })).describe("Checklist sections rendered as markdown").optional(), - refUrl: tool.schema.string().describe("Optional issue URL to update").optional(), - comments: tool.schema.array(tool.schema.string()).describe("Optional issue comments to post").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - ticket_load(_: PluginInput["client"], __: MergedKompassConfig, _projectRoot: string, shell: Shell) { - const definition = createTicketLoadTool(shell); - return tool({ - description: definition.description, - args: { - source: tool.schema.string().describe("Issue URL, repo#id, #id, file path, or raw text"), - comments: tool.schema.boolean().describe("Include issue comments").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, -}; - -export async function createOpenCodeTools( - client: PluginInput["client"], - projectRoot: string, - navigator?: { - client: V2Client; - legacyClient: (directory: string) => LegacyClient; - projectID: string; - protocol: "v1" | "v2"; - }, -): Promise> { - const config = await loadMergedKompassConfig(projectRoot); - const tools: Record = {}; - const logger = createPluginLogger(client, projectRoot); - const shell = createNodeShell(projectRoot); - - const navigatorEnabled = config.adapters.opencode.navigator.enabled; - let agentNames: string[] | undefined; - if (navigatorEnabled && navigator) { - try { - const response = await navigator.client.v2.agent.list({ location: { directory: projectRoot } }); - agentNames = response.data?.data.map((agent) => agent.id); - } catch { - // Target-workspace validation remains authoritative if discovery is unavailable during registration. - } - } - const navigatorTools: Record = navigatorEnabled && navigator - ? createNavigatorTools({ - agentNames, - client: navigator.client, - legacyClient: navigator.legacyClient, - projectID: navigator.projectID, - checkout: projectRoot, - config: config.adapters.opencode.navigator, - protocol: navigator.protocol, - }) - : {}; - - for (const toolName of getEnabledToolNames(config.tools, navigatorEnabled)) { - const creator = opencodeToolCreators[toolName as keyof typeof opencodeToolCreators]; - const navigatorTool = navigatorTools[toolName]; - if (creator || navigatorTool) { - const registeredName = getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name); - tools[registeredName] = navigatorTool ?? creator(client, config, projectRoot, shell); - logger.info("Loaded Kompass tool", { - tool: toolName, - registeredName, - }); - } - } - - return tools; -} - -export const OpenCodeCompassPlugin: Plugin = async (input: PluginInput) => { - const { client, worktree } = input; - const logger = createPluginLogger(client, worktree); - - logger.info("Initialized Kompass plugin", { - directory: getString(input.directory), - worktree: getString(worktree), - projectPath: getString((input as { project?: { path?: string } }).project?.path), - }); - - async function createToolsSafely() { - try { - const config = await loadMergedKompassConfig(worktree); - let navigator: { - client: V2Client; - legacyClient: (directory: string) => LegacyClient; - projectID: string; - protocol: "v1" | "v2"; - } | undefined; - if (config.adapters.opencode.navigator.enabled) { - const projectID = getString(input.project?.id); - if (!input.serverUrl || !projectID) { - logger.warn("Navigator requires OpenCode 1.17.12 or newer; Navigator tools were not registered"); - } else { - const legacyClients = new Map(); - const navigatorClient = createV2Client({ baseUrl: input.serverUrl.toString() }); - const protocol = await detectNavigatorProtocol(navigatorClient); - navigator = { - // A client-wide directory would silently hide sessions in managed worktrees. - client: navigatorClient, - legacyClient(directory) { - let located = legacyClients.get(directory); - if (!located) { - located = createLegacyClient({ baseUrl: input.serverUrl!.toString(), directory }); - legacyClients.set(directory, located); - } - return located; - }, - projectID, - protocol, - }; - const warning = await getNavigatorCompatibilityWarning( - navigator.client, - navigator.protocol, - navigator.legacyClient(worktree), - ); - if (warning) { - logger.warn(`${warning}; Navigator tools were not registered`); - navigator = undefined; - } - } - } - return await createOpenCodeTools(client, worktree, navigator); - } catch (error) { - logger.warn("Skipping Kompass tool registration", { - ...getErrorDetails(error), - }); - return {}; - } - } - - async function runConfigStep(name: string, register: () => Promise) { - try { - await register(); - } catch (error) { - logger.warn("Skipping Kompass config registration step", { - step: name, - worktree, - ...getErrorDetails(error), - }); - } - } - - const tools = await createToolsSafely(); - await registerRiftWorkspaceAdapter(input as never, logger); - - return { - tool: tools, - async config(cfg) { - await runConfigStep("agents", () => applyAgentsConfig(cfg, worktree, { logger })); - await runConfigStep("commands", () => applyCommandsConfig(cfg, worktree, { logger })); - }, - async "command.execute.before"(input, output) { - try { - const commandExecution = getCommandExecution(input, output); - - if (!commandExecution) return; - - logger.info("Executing Kompass command", commandExecution as Record); - } catch (error) { - logObservedFailure(logger, "command.execute.before hook failed", error, { - command: input.command, - arguments: input.arguments, - sessionID: input.sessionID, - }); - } - }, - }; +export { + OpenCodeCompassPluginV1, + OpenCodeCompassPluginV2, + setupOpenCodeV2, }; - -export { applyAgentsConfig, applyCommandsConfig }; +export { + applyAgentsConfig, + applyCommandsConfig, + createOpenCodeTools, + getCommandExecution, +} from "./v1.ts"; export default OpenCodeCompassPlugin; diff --git a/packages/opencode/legacy-plugin.ts b/packages/opencode/legacy-plugin.ts new file mode 100644 index 0000000..2cea899 --- /dev/null +++ b/packages/opencode/legacy-plugin.ts @@ -0,0 +1,63 @@ +import type { Config as SDKConfig, Project, createOpencodeClient } from "@opencode-ai/sdk"; +import { z } from "zod"; + +export type ToolContext = { + sessionID: string; + messageID: string; + agent: string; + directory: string; + worktree: string; + abort: AbortSignal; + metadata(input: { title?: string; metadata?: Record }): void; + ask(input: { + permission: string; + patterns: string[]; + always: string[]; + metadata: Record; + }): Promise; +}; + +export type ToolResult = string | { + title?: string; + output: string; + metadata?: Record; +}; + +export function tool(input: { + description: string; + args: Args; + execute(args: z.infer>, context: ToolContext): Promise; +}) { + return input; +} + +tool.schema = z; + +export type ToolDefinition = ReturnType; + +export type Config = Omit & { + plugin?: Array]>; +}; + +export type PluginInput = { + client: ReturnType; + project: Project; + directory: string; + worktree: string; + experimental_workspace: { + register(type: string, adapter: unknown): void; + }; + serverUrl: URL; + $: unknown; +}; + +export type Hooks = { + config?: (config: Config) => Promise; + tool?: Record; + "command.execute.before"?: ( + input: { command: string; sessionID: string; arguments: string }, + output: { parts: Array<{ type: string; text?: string; [key: string]: unknown }> }, + ) => Promise; +}; + +export type Plugin = (input: PluginInput, options?: Record) => Promise; diff --git a/packages/opencode/logging.ts b/packages/opencode/logging.ts index aedfaed..70eece3 100644 --- a/packages/opencode/logging.ts +++ b/packages/opencode/logging.ts @@ -1,4 +1,4 @@ -import type { PluginInput } from "@opencode-ai/plugin"; +import type { PluginInput } from "./legacy-plugin.ts"; type LogLevel = "debug" | "info" | "warn" | "error"; @@ -21,9 +21,23 @@ export function getErrorDetails(error: unknown): Record { } export function createPluginLogger( - client: PluginInput["client"], - directory: string, + clientOrDirectory: PluginInput["client"] | string, + directory = typeof clientOrDirectory === "string" ? clientOrDirectory : "", ): PluginLogger { + if (typeof clientOrDirectory === "string") { + function write(level: LogLevel, message: string, extra?: Record) { + const output = extra ? [`[kompass] ${message}`, { directory, ...extra }] : [`[kompass] ${message}`]; + console[level](...output); + } + return { + debug: (message, extra) => write("debug", message, extra), + info: (message, extra) => write("info", message, extra), + warn: (message, extra) => write("warn", message, extra), + error: (message, extra) => write("error", message, extra), + }; + } + const client = clientOrDirectory; + function dispatch(level: LogLevel, message: string, extra?: Record) { void client.app.log({ query: { directory }, @@ -33,7 +47,7 @@ export function createPluginLogger( message: `[kompass] ${message}`, ...(extra ? { extra } : {}), }, - }).catch((err) => { + }).catch((err: unknown) => { // Log to console for debugging; plugin behavior must not depend on logging. console.error("[kompass] Log write failed:", err); }); diff --git a/packages/opencode/navigator.ts b/packages/opencode/navigator.ts index 8f79a60..e03a655 100644 --- a/packages/opencode/navigator.ts +++ b/packages/opencode/navigator.ts @@ -1,5 +1,4 @@ -import type { ToolContext, ToolDefinition } from "@opencode-ai/plugin/tool"; -import { tool } from "@opencode-ai/plugin/tool"; +import { tool, type ToolContext, type ToolDefinition } from "./legacy-plugin.ts"; import type { Message as LegacyMessage, OpencodeClient as LegacyOpencodeClient, diff --git a/packages/opencode/package.json b/packages/opencode/package.json index b1c0bca..562d271 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -46,16 +46,13 @@ "bugs": { "url": "https://github.com/kompassdev/kompass/issues" }, - "peerDependencies": { - "@opencode-ai/plugin": "^1.18.7", - "@opencode-ai/sdk": "^1.18.7" - }, "dependencies": { - "rift-snapshot": "^0.0.10" + "@opencode-ai/plugin": "0.0.0-beta-18684", + "@opencode-ai/sdk": "^1.18.7", + "rift-snapshot": "^0.0.10", + "zod": "^4.1.8" }, "devDependencies": { - "@opencode-ai/plugin": "^1.18.7", - "@opencode-ai/sdk": "^1.18.7", "yaml": "^2.8.2" } } diff --git a/packages/opencode/plugin.ts b/packages/opencode/plugin.ts index 74c8790..a59dc17 100644 --- a/packages/opencode/plugin.ts +++ b/packages/opencode/plugin.ts @@ -1 +1,7 @@ -export { OpenCodeCompassPlugin, OpenCodeCompassPlugin as default } from "./index.ts"; +export { + OpenCodeCompassPlugin, + OpenCodeCompassPlugin as default, + OpenCodeCompassPluginV1, + OpenCodeCompassPluginV2, + setupOpenCodeV2, +} from "./index.ts"; diff --git a/packages/opencode/scripts/build.ts b/packages/opencode/scripts/build.ts index 300c46b..e250a71 100644 --- a/packages/opencode/scripts/build.ts +++ b/packages/opencode/scripts/build.ts @@ -15,7 +15,6 @@ const packageReadme = path.join(packageRoot, "README.md"); const runtimeDirs = ["agents", "commands", "components"] as const; const bundleExternals = [ "@opencode-ai/plugin", - "@opencode-ai/plugin/tool", "@opencode-ai/sdk/v2", ] as const; const bundleArgs = [ diff --git a/packages/opencode/test/agents-config.test.ts b/packages/opencode/test/agents-config.test.ts index 7c5f9c9..80875e2 100644 --- a/packages/opencode/test/agents-config.test.ts +++ b/packages/opencode/test/agents-config.test.ts @@ -45,7 +45,6 @@ describe("applyAgentsConfig", () => { todowrite: "allow", }); assert.equal(cfg.agent.worker?.prompt, undefined); - assert.equal(cfg.agent.navigator, undefined); assert.match(cfg.agent.reviewer?.prompt ?? "", /Never switch branches/i); }); diff --git a/packages/opencode/test/plugin-entry.test.ts b/packages/opencode/test/plugin-entry.test.ts index 8682437..b6b234b 100644 --- a/packages/opencode/test/plugin-entry.test.ts +++ b/packages/opencode/test/plugin-entry.test.ts @@ -2,13 +2,23 @@ import { describe, test } from "node:test"; import assert from "node:assert/strict"; describe("plugin entry", () => { - test("only exposes plugin factories", async () => { + test("exposes one dual-version plugin definition", async () => { const mod = await import("../plugin.ts"); assert.deepEqual( Object.keys(mod).sort(), - ["OpenCodeCompassPlugin", "default"], + [ + "OpenCodeCompassPlugin", + "OpenCodeCompassPluginV1", + "OpenCodeCompassPluginV2", + "default", + "setupOpenCodeV2", + ], ); assert.equal(mod.default, mod.OpenCodeCompassPlugin); + assert.equal(mod.default.server, mod.OpenCodeCompassPluginV1); + assert.equal(mod.default.setup, mod.setupOpenCodeV2); + assert.equal(mod.default.id, "kompass"); + assert.equal(mod.OpenCodeCompassPluginV2.setup, mod.setupOpenCodeV2); }); }); diff --git a/packages/opencode/test/tool-registration.test.ts b/packages/opencode/test/tool-registration.test.ts index d9f8562..0f81cb8 100644 --- a/packages/opencode/test/tool-registration.test.ts +++ b/packages/opencode/test/tool-registration.test.ts @@ -6,7 +6,7 @@ import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; -import { createOpenCodeTools, OpenCodeCompassPlugin } from "../index.ts"; +import { createOpenCodeTools, OpenCodeCompassPluginV1 } from "../index.ts"; import { createRiftWorkspaceAdapter, resolveRiftSourceDirectory } from "../rift-workspace.ts"; const execFileAsync = promisify(execFile); @@ -535,7 +535,7 @@ describe("createOpenCodeTools", () => { const tempDir = await createTempGitRepo(); try { - const plugin = await OpenCodeCompassPlugin({ + const plugin = await OpenCodeCompassPluginV1({ client: createMockClient() as never, directory: tempDir, worktree: tempDir, diff --git a/packages/opencode/test/v2-plugin.test.ts b/packages/opencode/test/v2-plugin.test.ts new file mode 100644 index 0000000..4ca0ccd --- /dev/null +++ b/packages/opencode/test/v2-plugin.test.ts @@ -0,0 +1,103 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; + +import { + OpenCodeCompassPluginV2, + setupOpenCodeV2, +} from "../index.ts"; + +describe("OpenCode v2 plugin", () => { + test("exports the v2 plugin definition", () => { + assert.equal(OpenCodeCompassPluginV2.id, "kompass"); + assert.equal(OpenCodeCompassPluginV2.setup, setupOpenCodeV2); + }); + + test("registers location-scoped agents, commands, and tools", async () => { + const agents: string[] = []; + const commands = new Map }>(); + const tools = new Map(); + const prompts: Array> = []; + const switchedAgents: string[] = []; + const createdSessions: Array> = []; + const availableAgents = new Map([ + ["worker", { id: "worker", permissions: [] as unknown[] }], + ["planner", { id: "planner", permissions: [] as unknown[] }], + ["reviewer", { id: "reviewer", permissions: [] as unknown[] }], + ]); + const context = { + location: { directory: process.cwd() }, + agent: { + transform: async (transform: (draft: unknown) => void) => transform({ + get(name: string) { + return availableAgents.get(name); + }, + update(name: string, update: (agent: Record) => void) { + agents.push(name); + update(availableAgents.get(name) as Record); + }, + }), + reload: async () => {}, + }, + command: { + transform: async (transform: (draft: unknown) => void) => transform({ + add(command: { name: string; execute(input: unknown): Promise }) { + commands.set(command.name, command); + }, + }), + reload: async () => {}, + }, + tool: { + transform: async (transform: (draft: unknown) => void) => transform({ + add(tool: { name: string; input: unknown }) { + tools.set(tool.name, tool); + }, + }), + reload: async () => {}, + }, + session: { + create: async (input: Record) => { + createdSessions.push(input); + return { id: "child-1" }; + }, + switchAgent: async ({ agent }: { agent: string }) => { + switchedAgents.push(agent); + }, + prompt: async (input: Record) => { + prompts.push(input); + }, + }, + }; + + await setupOpenCodeV2(context as never); + assert.ok(agents.includes("worker")); + assert.ok(commands.has("dev")); + assert.deepEqual([...tools.keys()].sort(), [ + "kompass_changes_load", + "kompass_pr_load", + "kompass_pr_load_review", + "kompass_pr_sync", + "kompass_ticket_load", + "kompass_ticket_sync", + ]); + assert.equal(typeof (tools.get("kompass_changes_load")?.input as any)?.["~standard"]?.validate, "function"); + + await commands.get("dev")?.execute({ + sessionID: "session-1", + prompt: { text: "fix auth" }, + delivery: "steer", + }); + assert.equal(createdSessions.length, 1); + assert.equal(createdSessions[0]?.agent, "worker"); + assert.equal(switchedAgents.length, 0); + assert.equal(prompts[0]?.sessionID, "child-1"); + assert.match(String(prompts[0]?.text), /fix auth/); + + await commands.get("learn")?.execute({ + sessionID: "session-1", + prompt: { text: "remember this" }, + delivery: "steer", + }); + assert.deepEqual(switchedAgents, ["worker"]); + assert.equal(prompts[1]?.sessionID, "session-1"); + }); +}); diff --git a/packages/opencode/v1.ts b/packages/opencode/v1.ts new file mode 100644 index 0000000..c93e555 --- /dev/null +++ b/packages/opencode/v1.ts @@ -0,0 +1,478 @@ +import type { Hooks, Plugin, PluginInput } from "./legacy-plugin.ts"; +import { tool, type ToolDefinition } from "./legacy-plugin.ts"; +import { createOpencodeClient as createLegacyClient, type OpencodeClient as LegacyClient } from "@opencode-ai/sdk/client"; +import { createOpencodeClient as createV2Client, type OpencodeClient as V2Client } from "@opencode-ai/sdk/v2"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +import { + createChangesLoadTool, + createPrLoadTool, + createPrLoadReviewTool, + createPrSyncTool, + createTicketLoadTool, + createTicketSyncTool, + getEnabledToolNames, + type MergedKompassConfig, + type Shell, + type ShellPromise, +} from "../core/index.ts"; +import { loadMergedKompassConfig } from "./cache.ts"; +import { applyAgentsConfig, applyCommandsConfig } from "./config.ts"; +import { createPluginLogger, getErrorDetails, type PluginLogger } from "./logging.ts"; +import { createNavigatorTools, detectNavigatorProtocol, getNavigatorCompatibilityWarning } from "./navigator.ts"; +import { registerRiftWorkspaceAdapter } from "./rift-workspace.ts"; +import { + getConfiguredOpenCodeToolName, +} from "./tool-names.ts"; + +const execFileAsync = promisify(execFile); + +type CommandExecuteBeforeHook = NonNullable; +type CommandExecuteBeforeInput = Parameters[0]; +type CommandExecuteBeforeOutput = Parameters[1]; +type OpenCodeToolCreator = ( + config: MergedKompassConfig, + shell: Shell, +) => ToolDefinition; + +type ShellResult = ShellPromise & { + stdout: Buffer; +}; + +function shellEscape(value: unknown): string { + return `'${String(value).replaceAll("'", `'\\''`)}'`; +} + +function shellResult(stdout: string, stderr: string, exitCode: number): ShellResult { + return { + cwd: () => { + throw new Error("Cannot change cwd after execution"); + }, + quiet: () => { + throw new Error("Cannot change quiet after execution"); + }, + nothrow: () => { + throw new Error("Cannot change nothrow after execution"); + }, + text: () => stdout, + json: () => JSON.parse(stdout), + exitCode, + stderr: Buffer.from(stderr), + stdout: Buffer.from(stdout), + }; +} + +class NodeShellCommand implements PromiseLike { + #command: string; + #cwd: string; + #quiet = false; + #nothrow = false; + #result?: Promise; + + constructor(command: string, cwd: string) { + this.#command = command; + this.#cwd = cwd; + } + + cwd(dir: string) { + this.#cwd = dir; + return this; + } + + quiet() { + this.#quiet = true; + return this; + } + + nothrow() { + this.#nothrow = true; + return this; + } + + then( + onfulfilled?: ((value: ShellResult) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ) { + return this.run().then(onfulfilled, onrejected); + } + + private run() { + this.#result ??= this.execute(); + return this.#result; + } + + private async execute() { + try { + const { stdout, stderr } = await execFileAsync("/bin/bash", ["-lc", this.#command], { + cwd: this.#cwd, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + }); + + this.write(stdout, stderr); + return shellResult(stdout, stderr, 0); + } catch (error) { + const failed = error as { stdout?: unknown; stderr?: unknown; message?: unknown; code?: unknown }; + const stdout = String(failed.stdout ?? ""); + const stderr = String(failed.stderr ?? failed.message ?? ""); + const exitCode = typeof failed.code === "number" ? failed.code : 1; + const result = shellResult(stdout, stderr, exitCode); + + this.write(stdout, stderr); + if (this.#nothrow) return result; + + throw new Error(stderr || `Command failed: ${this.#command}`); + } + } + + private write(stdout: string, stderr: string) { + if (this.#quiet) return; + if (stdout) process.stdout.write(stdout); + if (stderr) process.stderr.write(stderr); + } +} + +export function createNodeShell(defaultDirectory: string): Shell { + return (strings: TemplateStringsArray, ...expressions: unknown[]) => { + let command = strings[0] ?? ""; + + expressions.forEach((expression, index) => { + command += Array.isArray(expression) + ? expression.map((item) => shellEscape(item)).join(" ") + : shellEscape(expression); + command += strings[index + 1] ?? ""; + }); + + return new NodeShellCommand(command, defaultDirectory) as unknown as ShellPromise; + }; +} + +export type CommandExecution = { + command: string; + arguments: string; + prompt: string; +}; + +function getString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function logObservedFailure( + logger: PluginLogger, + message: string, + error: unknown, + extra?: Record, +) { + logger.warn(message, { + ...(extra ?? {}), + ...getErrorDetails(error), + }); +} + +export function getCommandExecution( + input: CommandExecuteBeforeInput, + output: CommandExecuteBeforeOutput, +): CommandExecution | undefined { + const prompt = output.parts + .flatMap((part) => part.type === "text" ? [part.text] : []) + .join("\n") + .trim(); + + if (!prompt) return; + + return { + command: input.command, + arguments: input.arguments, + prompt, + }; +} + +const opencodeToolCreators: Record = { + changes_load(_: MergedKompassConfig, shell: Shell) { + const definition = createChangesLoadTool(shell); + return tool({ + description: definition.description, + args: { + base: tool.schema.string().describe("Base branch or ref").optional(), + head: tool.schema.string().describe("Head branch, commit, or ref override").optional(), + depthHint: tool.schema.number().int().positive() + .describe("Optional shallow-fetch hint, such as PR commit count") + .optional(), + uncommitted: tool.schema.boolean() + .describe("Only load uncommitted changes (staged and unstaged), never fall back to branch comparison") + .optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + pr_load(_: MergedKompassConfig, shell: Shell) { + const definition = createPrLoadTool(shell); + return tool({ + description: definition.description, + args: { + pr: tool.schema.string().describe("PR number or URL").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + pr_load_review(_: MergedKompassConfig, shell: Shell) { + const definition = createPrLoadReviewTool(shell); + return tool({ + description: definition.description, + args: { + pr: tool.schema.string().describe("PR number or URL").optional(), + since: tool.schema.string().describe("Exclusive ISO-8601 review checkpoint"), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + pr_sync(config: MergedKompassConfig, shell: Shell) { + const definition = createPrSyncTool(shell); + + return tool({ + description: definition.description, + args: { + title: tool.schema.string().describe("PR title; required when creating or renaming a PR").optional(), + body: tool.schema.string().describe("PR body override").optional(), + description: tool.schema.string().describe("Short PR description rendered above checklist sections").optional(), + base: tool.schema.string().describe("Base branch to merge into").optional(), + head: tool.schema.string().describe("Head branch to use when creating a PR").optional(), + labels: tool.schema.array(tool.schema.string()).describe("Labels to add to the PR").optional(), + removeLabels: tool.schema.array(tool.schema.string()).describe("Labels to remove from an existing PR").optional(), + replaceLabels: tool.schema.array(tool.schema.string()).describe("Exact label set for the PR; an empty array clears all labels").optional(), + assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the PR").optional(), + checklists: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist section name"), + items: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist item name"), + completed: tool.schema.boolean().describe("Whether the item is completed"), + })).describe("Checklist items"), + })).describe("Checklist sections rendered as markdown").optional(), + draft: tool.schema.boolean().describe("Create as draft PR").optional(), + refUrl: tool.schema.string().describe("Optional PR URL to update").optional(), + commitId: tool.schema.string().describe("Commit SHA to anchor review comments to; omit unless sending review comments").optional(), + review: tool.schema.object({ + body: tool.schema.string().describe("Optional review summary body").optional(), + comments: tool.schema.array(tool.schema.object({ + path: tool.schema.string().describe("Changed file path"), + body: tool.schema.string().describe("Inline review comment body"), + line: tool.schema.number().int().describe("Ending line on the diff side"), + startLine: tool.schema.number().int().describe("Starting line for multi-line comments").optional(), + side: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the ending line").optional(), + startSide: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the starting line").optional(), + })).describe("Inline review comments to submit").optional(), + ...(config.shared.prApprove + ? { approve: tool.schema.boolean().describe("Approve the PR with this review comment").optional() } + : {} + ), + }).describe("Optional structured review submission; omit the field entirely unless submitting a review body, inline comments, or approval").optional(), + replies: tool.schema.array(tool.schema.object({ + inReplyTo: tool.schema.number().int().describe("Existing review comment ID to reply to"), + body: tool.schema.string().describe("Reply body"), + })).describe("Replies to existing review comments").optional(), + commentBody: tool.schema.string().describe("General PR comment body").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + ticket_sync(_: MergedKompassConfig, shell: Shell) { + const definition = createTicketSyncTool(shell); + return tool({ + description: definition.description, + args: { + title: tool.schema.string().describe("Issue title").optional(), + body: tool.schema.string().describe("Issue body override").optional(), + description: tool.schema.string().describe("Issue description rendered above checklist sections").optional(), + labels: tool.schema.array(tool.schema.string()).describe("Labels to apply to the issue").optional(), + assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the issue").optional(), + checklists: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist section name"), + items: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist item name"), + completed: tool.schema.boolean().describe("Whether the item is completed"), + })).describe("Checklist items"), + })).describe("Checklist sections rendered as markdown").optional(), + refUrl: tool.schema.string().describe("Optional issue URL to update").optional(), + comments: tool.schema.array(tool.schema.string()).describe("Optional issue comments to post").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + ticket_load(_: MergedKompassConfig, shell: Shell) { + const definition = createTicketLoadTool(shell); + return tool({ + description: definition.description, + args: { + source: tool.schema.string().describe("Issue URL, repo#id, #id, file path, or raw text"), + comments: tool.schema.boolean().describe("Include issue comments").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, +}; + +export function createCoreOpenCodeToolDefinitions( + config: MergedKompassConfig, + projectRoot: string, +): Record { + const shell = createNodeShell(projectRoot); + return Object.fromEntries( + Object.entries(opencodeToolCreators).map(([name, create]) => [name, create(config, shell)]), + ); +} + +export async function createOpenCodeTools( + client: PluginInput["client"], + projectRoot: string, + navigator?: { + client: V2Client; + legacyClient: (directory: string) => LegacyClient; + projectID: string; + protocol: "v1" | "v2"; + }, +): Promise> { + const config = await loadMergedKompassConfig(projectRoot); + const tools: Record = {}; + const logger = createPluginLogger(client, projectRoot); + const coreTools = createCoreOpenCodeToolDefinitions(config, projectRoot); + + const navigatorEnabled = config.adapters.opencode.navigator.enabled; + let agentNames: string[] | undefined; + if (navigatorEnabled && navigator) { + try { + const response = await navigator.client.v2.agent.list({ location: { directory: projectRoot } }); + agentNames = response.data?.data.map((agent) => agent.id); + } catch { + // Target-workspace validation remains authoritative if discovery is unavailable during registration. + } + } + const navigatorTools: Record = navigatorEnabled && navigator + ? createNavigatorTools({ + agentNames, + client: navigator.client, + legacyClient: navigator.legacyClient, + projectID: navigator.projectID, + checkout: projectRoot, + config: config.adapters.opencode.navigator, + protocol: navigator.protocol, + }) + : {}; + + for (const toolName of getEnabledToolNames(config.tools, navigatorEnabled)) { + const coreTool = coreTools[toolName]; + const navigatorTool = navigatorTools[toolName]; + if (coreTool || navigatorTool) { + const registeredName = getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name); + tools[registeredName] = navigatorTool ?? coreTool; + logger.info("Loaded Kompass tool", { + tool: toolName, + registeredName, + }); + } + } + + return tools; +} + +export const OpenCodeCompassPluginV1: Plugin = async (input: PluginInput) => { + const { client, worktree } = input; + const logger = createPluginLogger(client, worktree); + + logger.info("Initialized Kompass plugin", { + directory: getString(input.directory), + worktree: getString(worktree), + projectPath: getString((input as { project?: { path?: string } }).project?.path), + }); + + async function createToolsSafely() { + try { + const config = await loadMergedKompassConfig(worktree); + let navigator: { + client: V2Client; + legacyClient: (directory: string) => LegacyClient; + projectID: string; + protocol: "v1" | "v2"; + } | undefined; + if (config.adapters.opencode.navigator.enabled) { + const projectID = getString(input.project?.id); + if (!input.serverUrl || !projectID) { + logger.warn("Navigator requires OpenCode 1.17.12 or newer; Navigator tools were not registered"); + } else { + const legacyClients = new Map(); + const navigatorClient = createV2Client({ baseUrl: input.serverUrl.toString() }); + const protocol = await detectNavigatorProtocol(navigatorClient); + navigator = { + client: navigatorClient, + legacyClient(directory) { + let located = legacyClients.get(directory); + if (!located) { + located = createLegacyClient({ baseUrl: input.serverUrl.toString(), directory }); + legacyClients.set(directory, located); + } + return located; + }, + projectID, + protocol, + }; + const warning = await getNavigatorCompatibilityWarning( + navigator.client, + navigator.protocol, + navigator.legacyClient(worktree), + ); + if (warning) { + logger.warn(`${warning}; Navigator tools were not registered`); + navigator = undefined; + } + } + } + return await createOpenCodeTools(client, worktree, navigator); + } catch (error) { + logger.warn("Skipping Kompass tool registration", { + ...getErrorDetails(error), + }); + return {}; + } + } + + async function runConfigStep(name: string, register: () => Promise) { + try { + await register(); + } catch (error) { + logger.warn("Skipping Kompass config registration step", { + step: name, + worktree, + ...getErrorDetails(error), + }); + } + } + + const tools = await createToolsSafely(); + await registerRiftWorkspaceAdapter(input, logger); + + return { + tool: tools, + async config(cfg) { + await runConfigStep("agents", () => applyAgentsConfig(cfg, worktree, { logger })); + await runConfigStep("commands", () => applyCommandsConfig(cfg, worktree, { logger })); + }, + async "command.execute.before"(input, output) { + try { + const commandExecution = getCommandExecution(input, output); + + if (!commandExecution) return; + + logger.info("Executing Kompass command", commandExecution as Record); + } catch (error) { + logObservedFailure(logger, "command.execute.before hook failed", error, { + command: input.command, + arguments: input.arguments, + sessionID: input.sessionID, + }); + } + }, + }; +}; + +export { applyAgentsConfig, applyCommandsConfig }; +export default OpenCodeCompassPluginV1; diff --git a/packages/opencode/v2.ts b/packages/opencode/v2.ts new file mode 100644 index 0000000..09c771a --- /dev/null +++ b/packages/opencode/v2.ts @@ -0,0 +1,163 @@ +import { Plugin } from "@opencode-ai/plugin"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { z } from "zod"; + +import { + getEnabledToolNames, + type ResolvedAgentDefinition, + type ResolvedCommandDefinition, +} from "../core/index.ts"; +import { + loadMergedKompassConfig, + loadResolvedAgents, + loadResolvedCommands, +} from "./cache.ts"; +import { createPluginLogger } from "./logging.ts"; +import { getConfiguredOpenCodeToolName } from "./tool-names.ts"; +import { createCoreOpenCodeToolDefinitions } from "./v1.ts"; + +type PluginContext = Plugin.Context; +type ToolDraft = Parameters[0]>[0]; +type OpenCodeTool = Parameters[0]; +const execFileAsync = promisify(execFile); + +function toOpenCodeTool( + name: string, + definition: ReturnType[string], + projectRoot: string, +): OpenCodeTool { + return { + name, + description: definition.description, + input: z.object(definition.args) as OpenCodeTool["input"], + options: { codemode: false }, + async execute(args) { + const result = await definition.execute(args as never, { + worktree: projectRoot, + directory: projectRoot, + } as never); + const content = typeof result === "string" ? result : result.output; + return { output: JSON.parse(content), content }; + }, + }; +} + +export async function createOpenCodeV2Tools(projectRoot: string): Promise { + const config = await loadMergedKompassConfig(projectRoot); + const definitions = createCoreOpenCodeToolDefinitions(config, projectRoot); + + return getEnabledToolNames(config.tools).flatMap((toolName) => { + const definition = definitions[toolName]; + if (!definition) return []; + const name = getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name); + return [toOpenCodeTool(name, definition, projectRoot)]; + }); +} + +function applyAgents( + draft: Parameters[0]>[0], + agents: Record, +) { + for (const [name, definition] of Object.entries(agents)) { + if (!draft.get(name)) { + throw new Error(`OpenCode v2 agent "${name}" must be registered before Kompass setup`); + } + draft.update(name, (agent) => { + agent.description = definition.description; + agent.permissions.push(...Object.entries(definition.permission).map(([action, effect]) => ({ + action, + resource: "*", + effect: effect as "allow" | "ask" | "deny", + }))); + agent.system = definition.prompt; + if (definition.mode) agent.mode = definition.mode; + }); + } +} + +const argumentPattern = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi; +const placeholderPattern = /\$(\d+)/g; +const shellPattern = /!`([^`]+)`/g; + +async function expandCommand(template: string, input: string, projectRoot: string) { + const args = (input.match(argumentPattern) ?? []).map((arg) => arg.replace(/^['"]|['"]$/g, "")); + const placeholders = template.match(placeholderPattern) ?? []; + const last = Math.max(0, ...placeholders.map((item) => Number(item.slice(1)))); + const expanded = template.replaceAll(placeholderPattern, (_, rawIndex: string) => { + const index = Number(rawIndex) - 1; + if (index >= args.length) return ""; + return index + 1 === last ? args.slice(index).join(" ") : args[index] ?? ""; + }); + const withArguments = expanded.replaceAll("$ARGUMENTS", input); + const text = placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim() + ? `${withArguments}\n\n${input}`.trim() + : withArguments.trim(); + const matches = Array.from(text.matchAll(shellPattern)); + const outputs = await Promise.all(matches.map(async (match) => { + const { stdout, stderr } = await execFileAsync("/bin/sh", ["-lc", match[1] ?? ""], { + cwd: projectRoot, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + }); + return `${stdout}${stderr}`; + })); + const iterator = outputs[Symbol.iterator](); + return text.replace(shellPattern, () => iterator.next().value ?? ""); +} + +function applyCommands( + draft: Parameters[0]>[0], + commands: Record, + ctx: PluginContext, + projectRoot: string, +) { + for (const [name, definition] of Object.entries(commands)) { + draft.add({ + name, + description: definition.description, + async execute({ sessionID, prompt, delivery }) { + let targetSessionID = sessionID; + if (definition.subtask) { + const session = await ctx.session.create({ + agent: definition.agent, + location: { + directory: projectRoot, + ...(ctx.location.workspaceID ? { workspaceID: ctx.location.workspaceID } : {}), + }, + }); + targetSessionID = session.id as typeof sessionID; + } else if (definition.agent) { + await ctx.session.switchAgent({ sessionID, agent: definition.agent }); + } + await ctx.session.prompt({ + ...prompt, + sessionID: targetSessionID, + text: await expandCommand(definition.template, prompt.text, projectRoot), + delivery, + }); + }, + }); + } +} + +export async function setupOpenCodeV2(ctx: PluginContext) { + const projectRoot = ctx.location.directory; + const logger = createPluginLogger(projectRoot); + const [agents, commands, tools] = await Promise.all([ + loadResolvedAgents(projectRoot), + loadResolvedCommands(projectRoot), + createOpenCodeV2Tools(projectRoot), + ]); + await ctx.agent.transform((draft) => applyAgents(draft, agents)); + await ctx.command.transform((draft) => applyCommands(draft, commands, ctx, projectRoot)); + await ctx.tool.transform((draft) => { + for (const tool of tools) draft.add(tool); + }); + logger.info("Initialized Kompass v2 plugin", { directory: projectRoot, tools: tools.length }); +} + +export const OpenCodeCompassPluginV2 = Plugin.define({ + id: "kompass", + setup: setupOpenCodeV2, +}); diff --git a/packages/web/src/content/docs/docs/adapters/opencode.mdx b/packages/web/src/content/docs/docs/adapters/opencode.mdx index 74597aa..9979755 100644 --- a/packages/web/src/content/docs/docs/adapters/opencode.mdx +++ b/packages/web/src/content/docs/docs/adapters/opencode.mdx @@ -7,6 +7,10 @@ description: Use Kompass workflows inside OpenCode through the published adapter The OpenCode adapter package is `@kompassdev/opencode`. +It exports one definition for both plugin APIs. Configure it with `"plugin": ["@kompassdev/opencode"]` on OpenCode 1 or `"plugins": ["@kompassdev/opencode"]` on OpenCode 2. + +OpenCode 2 agent transforms are update-only. If an agent is renamed in Kompass config, the host must register that agent ID before Kompass setup runs. + ## What it provides Inside OpenCode, the adapter exposes: @@ -14,7 +18,7 @@ Inside OpenCode, the adapter exposes: - Kompass commands - Kompass agents - Kompass tools -- Kompass Navigator session and worktree orchestration +- Kompass Navigator session and worktree orchestration through the OpenCode 1 plugin API - project override loading from local config files ## Project overrides @@ -33,7 +37,7 @@ You can use overrides to: ## Kompass Navigator -Navigator is for explicitly requested native-session and managed-worktree workflows. It is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. Navigator follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on the same compatible API in the current checkout or managed Git worktrees. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer and is enabled by default. +Navigator is available through the OpenCode 1 plugin API for explicitly requested native-session and managed-worktree workflows. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required to register Navigator safely. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. Navigator follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on the same compatible API in the current checkout or managed Git worktrees. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer and is enabled by default on OpenCode 1. ```jsonc { diff --git a/packages/web/src/content/docs/docs/config/schema.mdx b/packages/web/src/content/docs/docs/config/schema.mdx index 69e2bc6..56eb9d3 100644 --- a/packages/web/src/content/docs/docs/config/schema.mdx +++ b/packages/web/src/content/docs/docs/config/schema.mdx @@ -9,7 +9,7 @@ The published schema lives in `kompass.schema.json`. - default base branch: `main` - OpenCode adapter agent mode: `all` -- OpenCode Navigator: enabled, with limits of 8 concurrent sessions, 20,000 read characters, 4,000 output characters per item, and 120,000 milliseconds per wait +- OpenCode 1 Navigator: enabled, with limits of 8 concurrent sessions, 20,000 read characters, 4,000 output characters per item, and 120,000 milliseconds per wait - OpenCode subtask command mode: `kompass` - bundled shared validation guidance is included by default diff --git a/packages/web/src/content/docs/docs/reference/tools/index.mdx b/packages/web/src/content/docs/docs/reference/tools/index.mdx index de32e78..c08db72 100644 --- a/packages/web/src/content/docs/docs/reference/tools/index.mdx +++ b/packages/web/src/content/docs/docs/reference/tools/index.mdx @@ -50,7 +50,7 @@ Creates or updates GitHub issues, renders checklist sections, and can append iss ## OpenCode Navigator tools -Navigator tools are enabled by default and are available only through the OpenCode adapter. Use them only for explicitly requested native-session or managed-worktree workflows, not for subagent delegation; OpenCode's built-in `task` tool owns subagent lifecycle and waiting. Their default runtime names use the `kompass_` prefix. See [Kompass Navigator](./navigator/) for arguments and safety behavior. +Navigator tools are enabled by default through the OpenCode 1 plugin API. OpenCode 2 does not yet expose the host APIs they require. Use them only for explicitly requested native-session or managed-worktree workflows, not for subagent delegation; OpenCode's built-in `task` tool owns subagent lifecycle and waiting. Their default runtime names use the `kompass_` prefix. See [Kompass Navigator](./navigator/) for arguments and safety behavior. ### `worktree_list` diff --git a/packages/web/src/content/docs/docs/reference/tools/navigator.mdx b/packages/web/src/content/docs/docs/reference/tools/navigator.mdx index 146d0fd..a1ee198 100644 --- a/packages/web/src/content/docs/docs/reference/tools/navigator.mdx +++ b/packages/web/src/content/docs/docs/reference/tools/navigator.mdx @@ -3,7 +3,7 @@ title: Kompass Navigator description: Native OpenCode session and managed-worktree orchestration tools. --- -Navigator is enabled by default in the OpenCode adapter. Use it only when the user explicitly requests native-session, managed-worktree, or multi-session workflow orchestration. Do not use it for subagent delegation: OpenCode's built-in `task` tool creates child sessions, waits for foreground tasks, and reports background completion automatically. Default runtime names use the `kompass_` prefix; set `adapters.opencode.navigator.enabled` to `false` to disable all eight tools. +Navigator is enabled by default through the OpenCode 1 plugin API. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required by these tools. Use Navigator only when the user explicitly requests native-session, managed-worktree, or multi-session workflow orchestration. Do not use it for subagent delegation: OpenCode's built-in `task` tool creates child sessions, waits for foreground tasks, and reports background completion automatically. Default runtime names use the `kompass_` prefix; set `adapters.opencode.navigator.enabled` to `false` to disable all eight tools. | Logical tool | Purpose | | --- | --- | From d3314820f3232a7752c0319207554d1e0f2f8189 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Mon, 31 Aug 2026 11:32:09 -0300 Subject: [PATCH 2/7] fix: preserve v2 shell substitution output --- packages/opencode/test/v2-plugin.test.ts | 11 +++++++++++ packages/opencode/v2.ts | 19 ++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/opencode/test/v2-plugin.test.ts b/packages/opencode/test/v2-plugin.test.ts index 4ca0ccd..6378dc3 100644 --- a/packages/opencode/test/v2-plugin.test.ts +++ b/packages/opencode/test/v2-plugin.test.ts @@ -5,6 +5,7 @@ import { OpenCodeCompassPluginV2, setupOpenCodeV2, } from "../index.ts"; +import { expandCommand } from "../v2.ts"; describe("OpenCode v2 plugin", () => { test("exports the v2 plugin definition", () => { @@ -12,6 +13,16 @@ describe("OpenCode v2 plugin", () => { assert.equal(OpenCodeCompassPluginV2.setup, setupOpenCodeV2); }); + test("inlines output from failing shell substitutions", async () => { + const prompt = await expandCommand( + "Result: !`printf stdout; printf stderr >&2; exit 1`", + "", + process.cwd(), + ); + + assert.equal(prompt, "Result: stdoutstderr"); + }); + test("registers location-scoped agents, commands, and tools", async () => { const agents: string[] = []; const commands = new Map }>(); diff --git a/packages/opencode/v2.ts b/packages/opencode/v2.ts index 09c771a..be67134 100644 --- a/packages/opencode/v2.ts +++ b/packages/opencode/v2.ts @@ -80,7 +80,7 @@ const argumentPattern = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi; const placeholderPattern = /\$(\d+)/g; const shellPattern = /!`([^`]+)`/g; -async function expandCommand(template: string, input: string, projectRoot: string) { +export async function expandCommand(template: string, input: string, projectRoot: string) { const args = (input.match(argumentPattern) ?? []).map((arg) => arg.replace(/^['"]|['"]$/g, "")); const placeholders = template.match(placeholderPattern) ?? []; const last = Math.max(0, ...placeholders.map((item) => Number(item.slice(1)))); @@ -95,12 +95,17 @@ async function expandCommand(template: string, input: string, projectRoot: strin : withArguments.trim(); const matches = Array.from(text.matchAll(shellPattern)); const outputs = await Promise.all(matches.map(async (match) => { - const { stdout, stderr } = await execFileAsync("/bin/sh", ["-lc", match[1] ?? ""], { - cwd: projectRoot, - encoding: "utf8", - maxBuffer: 10 * 1024 * 1024, - }); - return `${stdout}${stderr}`; + try { + const { stdout, stderr } = await execFileAsync("/bin/bash", ["-lc", match[1] ?? ""], { + cwd: projectRoot, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + }); + return `${stdout}${stderr}`; + } catch (error) { + const failed = error as { stdout?: unknown; stderr?: unknown; message?: unknown }; + return `${String(failed.stdout ?? "")}${String(failed.stderr ?? failed.message ?? "")}`; + } })); const iterator = outputs[Symbol.iterator](); return text.replace(shellPattern, () => iterator.next().value ?? ""); From 467056471c3c14b0245773488dbe59b658e1fe14 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Mon, 31 Aug 2026 12:13:10 -0300 Subject: [PATCH 3/7] refactor: decouple shared OpenCode tools --- packages/opencode/legacy-plugin.ts | 36 +--- packages/opencode/shared-tools.ts | 301 +++++++++++++++++++++++++++++ packages/opencode/v1.ts | 270 +------------------------- packages/opencode/v2.ts | 2 +- 4 files changed, 307 insertions(+), 302 deletions(-) create mode 100644 packages/opencode/shared-tools.ts diff --git a/packages/opencode/legacy-plugin.ts b/packages/opencode/legacy-plugin.ts index 2cea899..4096dfb 100644 --- a/packages/opencode/legacy-plugin.ts +++ b/packages/opencode/legacy-plugin.ts @@ -1,39 +1,7 @@ import type { Config as SDKConfig, Project, createOpencodeClient } from "@opencode-ai/sdk"; -import { z } from "zod"; +import type { ToolDefinition } from "./shared-tools.ts"; -export type ToolContext = { - sessionID: string; - messageID: string; - agent: string; - directory: string; - worktree: string; - abort: AbortSignal; - metadata(input: { title?: string; metadata?: Record }): void; - ask(input: { - permission: string; - patterns: string[]; - always: string[]; - metadata: Record; - }): Promise; -}; - -export type ToolResult = string | { - title?: string; - output: string; - metadata?: Record; -}; - -export function tool(input: { - description: string; - args: Args; - execute(args: z.infer>, context: ToolContext): Promise; -}) { - return input; -} - -tool.schema = z; - -export type ToolDefinition = ReturnType; +export { tool, type ToolContext, type ToolDefinition, type ToolResult } from "./shared-tools.ts"; export type Config = Omit & { plugin?: Array]>; diff --git a/packages/opencode/shared-tools.ts b/packages/opencode/shared-tools.ts new file mode 100644 index 0000000..411f2a6 --- /dev/null +++ b/packages/opencode/shared-tools.ts @@ -0,0 +1,301 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { z } from "zod"; + +import { + createChangesLoadTool, + createPrLoadReviewTool, + createPrLoadTool, + createPrSyncTool, + createTicketLoadTool, + createTicketSyncTool, + type MergedKompassConfig, + type Shell, + type ShellPromise, +} from "../core/index.ts"; + +export type ToolContext = { + sessionID: string; + messageID: string; + agent: string; + directory: string; + worktree: string; + abort: AbortSignal; + metadata(input: { title?: string; metadata?: Record }): void; + ask(input: { + permission: string; + patterns: string[]; + always: string[]; + metadata: Record; + }): Promise; +}; + +export type ToolResult = string | { + title?: string; + output: string; + metadata?: Record; +}; + +export function tool(input: { + description: string; + args: Args; + execute(args: z.infer>, context: ToolContext): Promise; +}) { + return input; +} + +tool.schema = z; + +export type ToolDefinition = ReturnType; + +const execFileAsync = promisify(execFile); + +type OpenCodeToolCreator = ( + config: MergedKompassConfig, + shell: Shell, +) => ToolDefinition; + +type ShellResult = ShellPromise & { + stdout: Buffer; +}; + +function shellEscape(value: unknown): string { + return `'${String(value).replaceAll("'", `'\\''`)}'`; +} + +function shellResult(stdout: string, stderr: string, exitCode: number): ShellResult { + return { + cwd: () => { + throw new Error("Cannot change cwd after execution"); + }, + quiet: () => { + throw new Error("Cannot change quiet after execution"); + }, + nothrow: () => { + throw new Error("Cannot change nothrow after execution"); + }, + text: () => stdout, + json: () => JSON.parse(stdout), + exitCode, + stderr: Buffer.from(stderr), + stdout: Buffer.from(stdout), + }; +} + +class NodeShellCommand implements PromiseLike { + #command: string; + #cwd: string; + #quiet = false; + #nothrow = false; + #result?: Promise; + + constructor(command: string, cwd: string) { + this.#command = command; + this.#cwd = cwd; + } + + cwd(dir: string) { + this.#cwd = dir; + return this; + } + + quiet() { + this.#quiet = true; + return this; + } + + nothrow() { + this.#nothrow = true; + return this; + } + + then( + onfulfilled?: ((value: ShellResult) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ) { + return this.run().then(onfulfilled, onrejected); + } + + private run() { + this.#result ??= this.execute(); + return this.#result; + } + + private async execute() { + try { + const { stdout, stderr } = await execFileAsync("/bin/bash", ["-lc", this.#command], { + cwd: this.#cwd, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + }); + + this.write(stdout, stderr); + return shellResult(stdout, stderr, 0); + } catch (error) { + const failed = error as { stdout?: unknown; stderr?: unknown; message?: unknown; code?: unknown }; + const stdout = String(failed.stdout ?? ""); + const stderr = String(failed.stderr ?? failed.message ?? ""); + const exitCode = typeof failed.code === "number" ? failed.code : 1; + const result = shellResult(stdout, stderr, exitCode); + + this.write(stdout, stderr); + if (this.#nothrow) return result; + + throw new Error(stderr || `Command failed: ${this.#command}`); + } + } + + private write(stdout: string, stderr: string) { + if (this.#quiet) return; + if (stdout) process.stdout.write(stdout); + if (stderr) process.stderr.write(stderr); + } +} + +export function createNodeShell(defaultDirectory: string): Shell { + return (strings: TemplateStringsArray, ...expressions: unknown[]) => { + let command = strings[0] ?? ""; + + expressions.forEach((expression, index) => { + command += Array.isArray(expression) + ? expression.map((item) => shellEscape(item)).join(" ") + : shellEscape(expression); + command += strings[index + 1] ?? ""; + }); + + return new NodeShellCommand(command, defaultDirectory) as unknown as ShellPromise; + }; +} + +const opencodeToolCreators: Record = { + changes_load(_: MergedKompassConfig, shell: Shell) { + const definition = createChangesLoadTool(shell); + return tool({ + description: definition.description, + args: { + base: tool.schema.string().describe("Base branch or ref").optional(), + head: tool.schema.string().describe("Head branch, commit, or ref override").optional(), + depthHint: tool.schema.number().int().positive() + .describe("Optional shallow-fetch hint, such as PR commit count") + .optional(), + uncommitted: tool.schema.boolean() + .describe("Only load uncommitted changes (staged and unstaged), never fall back to branch comparison") + .optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + pr_load(_: MergedKompassConfig, shell: Shell) { + const definition = createPrLoadTool(shell); + return tool({ + description: definition.description, + args: { + pr: tool.schema.string().describe("PR number or URL").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + pr_load_review(_: MergedKompassConfig, shell: Shell) { + const definition = createPrLoadReviewTool(shell); + return tool({ + description: definition.description, + args: { + pr: tool.schema.string().describe("PR number or URL").optional(), + since: tool.schema.string().describe("Exclusive ISO-8601 review checkpoint"), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + pr_sync(config: MergedKompassConfig, shell: Shell) { + const definition = createPrSyncTool(shell); + return tool({ + description: definition.description, + args: { + title: tool.schema.string().describe("PR title; required when creating or renaming a PR").optional(), + body: tool.schema.string().describe("PR body override").optional(), + description: tool.schema.string().describe("Short PR description rendered above checklist sections").optional(), + base: tool.schema.string().describe("Base branch to merge into").optional(), + head: tool.schema.string().describe("Head branch to use when creating a PR").optional(), + labels: tool.schema.array(tool.schema.string()).describe("Labels to add to the PR").optional(), + removeLabels: tool.schema.array(tool.schema.string()).describe("Labels to remove from an existing PR").optional(), + replaceLabels: tool.schema.array(tool.schema.string()).describe("Exact label set for the PR; an empty array clears all labels").optional(), + assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the PR").optional(), + checklists: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist section name"), + items: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist item name"), + completed: tool.schema.boolean().describe("Whether the item is completed"), + })).describe("Checklist items"), + })).describe("Checklist sections rendered as markdown").optional(), + draft: tool.schema.boolean().describe("Create as draft PR").optional(), + refUrl: tool.schema.string().describe("Optional PR URL to update").optional(), + commitId: tool.schema.string().describe("Commit SHA to anchor review comments to; omit unless sending review comments").optional(), + review: tool.schema.object({ + body: tool.schema.string().describe("Optional review summary body").optional(), + comments: tool.schema.array(tool.schema.object({ + path: tool.schema.string().describe("Changed file path"), + body: tool.schema.string().describe("Inline review comment body"), + line: tool.schema.number().int().describe("Ending line on the diff side"), + startLine: tool.schema.number().int().describe("Starting line for multi-line comments").optional(), + side: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the ending line").optional(), + startSide: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the starting line").optional(), + })).describe("Inline review comments to submit").optional(), + ...(config.shared.prApprove + ? { approve: tool.schema.boolean().describe("Approve the PR with this review comment").optional() } + : {} + ), + }).describe("Optional structured review submission; omit the field entirely unless submitting a review body, inline comments, or approval").optional(), + replies: tool.schema.array(tool.schema.object({ + inReplyTo: tool.schema.number().int().describe("Existing review comment ID to reply to"), + body: tool.schema.string().describe("Reply body"), + })).describe("Replies to existing review comments").optional(), + commentBody: tool.schema.string().describe("General PR comment body").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + ticket_sync(_: MergedKompassConfig, shell: Shell) { + const definition = createTicketSyncTool(shell); + return tool({ + description: definition.description, + args: { + title: tool.schema.string().describe("Issue title").optional(), + body: tool.schema.string().describe("Issue body override").optional(), + description: tool.schema.string().describe("Issue description rendered above checklist sections").optional(), + labels: tool.schema.array(tool.schema.string()).describe("Labels to apply to the issue").optional(), + assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the issue").optional(), + checklists: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist section name"), + items: tool.schema.array(tool.schema.object({ + name: tool.schema.string().describe("Checklist item name"), + completed: tool.schema.boolean().describe("Whether the item is completed"), + })).describe("Checklist items"), + })).describe("Checklist sections rendered as markdown").optional(), + refUrl: tool.schema.string().describe("Optional issue URL to update").optional(), + comments: tool.schema.array(tool.schema.string()).describe("Optional issue comments to post").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, + ticket_load(_: MergedKompassConfig, shell: Shell) { + const definition = createTicketLoadTool(shell); + return tool({ + description: definition.description, + args: { + source: tool.schema.string().describe("Issue URL, repo#id, #id, file path, or raw text"), + comments: tool.schema.boolean().describe("Include issue comments").optional(), + }, + execute: (args, context) => definition.execute(args, context), + }); + }, +}; + +export function createCoreOpenCodeToolDefinitions( + config: MergedKompassConfig, + projectRoot: string, +): Record { + const shell = createNodeShell(projectRoot); + return Object.fromEntries( + Object.entries(opencodeToolCreators).map(([name, create]) => [name, create(config, shell)]), + ); +} diff --git a/packages/opencode/v1.ts b/packages/opencode/v1.ts index c93e555..f4ac1af 100644 --- a/packages/opencode/v1.ts +++ b/packages/opencode/v1.ts @@ -1,153 +1,23 @@ import type { Hooks, Plugin, PluginInput } from "./legacy-plugin.ts"; -import { tool, type ToolDefinition } from "./legacy-plugin.ts"; import { createOpencodeClient as createLegacyClient, type OpencodeClient as LegacyClient } from "@opencode-ai/sdk/client"; import { createOpencodeClient as createV2Client, type OpencodeClient as V2Client } from "@opencode-ai/sdk/v2"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; import { - createChangesLoadTool, - createPrLoadTool, - createPrLoadReviewTool, - createPrSyncTool, - createTicketLoadTool, - createTicketSyncTool, getEnabledToolNames, - type MergedKompassConfig, - type Shell, - type ShellPromise, } from "../core/index.ts"; import { loadMergedKompassConfig } from "./cache.ts"; import { applyAgentsConfig, applyCommandsConfig } from "./config.ts"; import { createPluginLogger, getErrorDetails, type PluginLogger } from "./logging.ts"; import { createNavigatorTools, detectNavigatorProtocol, getNavigatorCompatibilityWarning } from "./navigator.ts"; import { registerRiftWorkspaceAdapter } from "./rift-workspace.ts"; -import { - getConfiguredOpenCodeToolName, -} from "./tool-names.ts"; +import { createCoreOpenCodeToolDefinitions, type ToolDefinition } from "./shared-tools.ts"; +import { getConfiguredOpenCodeToolName } from "./tool-names.ts"; -const execFileAsync = promisify(execFile); +export { createCoreOpenCodeToolDefinitions, createNodeShell } from "./shared-tools.ts"; type CommandExecuteBeforeHook = NonNullable; type CommandExecuteBeforeInput = Parameters[0]; type CommandExecuteBeforeOutput = Parameters[1]; -type OpenCodeToolCreator = ( - config: MergedKompassConfig, - shell: Shell, -) => ToolDefinition; - -type ShellResult = ShellPromise & { - stdout: Buffer; -}; - -function shellEscape(value: unknown): string { - return `'${String(value).replaceAll("'", `'\\''`)}'`; -} - -function shellResult(stdout: string, stderr: string, exitCode: number): ShellResult { - return { - cwd: () => { - throw new Error("Cannot change cwd after execution"); - }, - quiet: () => { - throw new Error("Cannot change quiet after execution"); - }, - nothrow: () => { - throw new Error("Cannot change nothrow after execution"); - }, - text: () => stdout, - json: () => JSON.parse(stdout), - exitCode, - stderr: Buffer.from(stderr), - stdout: Buffer.from(stdout), - }; -} - -class NodeShellCommand implements PromiseLike { - #command: string; - #cwd: string; - #quiet = false; - #nothrow = false; - #result?: Promise; - - constructor(command: string, cwd: string) { - this.#command = command; - this.#cwd = cwd; - } - - cwd(dir: string) { - this.#cwd = dir; - return this; - } - - quiet() { - this.#quiet = true; - return this; - } - - nothrow() { - this.#nothrow = true; - return this; - } - - then( - onfulfilled?: ((value: ShellResult) => TResult1 | PromiseLike) | null, - onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, - ) { - return this.run().then(onfulfilled, onrejected); - } - - private run() { - this.#result ??= this.execute(); - return this.#result; - } - - private async execute() { - try { - const { stdout, stderr } = await execFileAsync("/bin/bash", ["-lc", this.#command], { - cwd: this.#cwd, - encoding: "utf8", - maxBuffer: 10 * 1024 * 1024, - }); - - this.write(stdout, stderr); - return shellResult(stdout, stderr, 0); - } catch (error) { - const failed = error as { stdout?: unknown; stderr?: unknown; message?: unknown; code?: unknown }; - const stdout = String(failed.stdout ?? ""); - const stderr = String(failed.stderr ?? failed.message ?? ""); - const exitCode = typeof failed.code === "number" ? failed.code : 1; - const result = shellResult(stdout, stderr, exitCode); - - this.write(stdout, stderr); - if (this.#nothrow) return result; - - throw new Error(stderr || `Command failed: ${this.#command}`); - } - } - - private write(stdout: string, stderr: string) { - if (this.#quiet) return; - if (stdout) process.stdout.write(stdout); - if (stderr) process.stderr.write(stderr); - } -} - -export function createNodeShell(defaultDirectory: string): Shell { - return (strings: TemplateStringsArray, ...expressions: unknown[]) => { - let command = strings[0] ?? ""; - - expressions.forEach((expression, index) => { - command += Array.isArray(expression) - ? expression.map((item) => shellEscape(item)).join(" ") - : shellEscape(expression); - command += strings[index + 1] ?? ""; - }); - - return new NodeShellCommand(command, defaultDirectory) as unknown as ShellPromise; - }; -} - export type CommandExecution = { command: string; arguments: string; @@ -188,140 +58,6 @@ export function getCommandExecution( }; } -const opencodeToolCreators: Record = { - changes_load(_: MergedKompassConfig, shell: Shell) { - const definition = createChangesLoadTool(shell); - return tool({ - description: definition.description, - args: { - base: tool.schema.string().describe("Base branch or ref").optional(), - head: tool.schema.string().describe("Head branch, commit, or ref override").optional(), - depthHint: tool.schema.number().int().positive() - .describe("Optional shallow-fetch hint, such as PR commit count") - .optional(), - uncommitted: tool.schema.boolean() - .describe("Only load uncommitted changes (staged and unstaged), never fall back to branch comparison") - .optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - pr_load(_: MergedKompassConfig, shell: Shell) { - const definition = createPrLoadTool(shell); - return tool({ - description: definition.description, - args: { - pr: tool.schema.string().describe("PR number or URL").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - pr_load_review(_: MergedKompassConfig, shell: Shell) { - const definition = createPrLoadReviewTool(shell); - return tool({ - description: definition.description, - args: { - pr: tool.schema.string().describe("PR number or URL").optional(), - since: tool.schema.string().describe("Exclusive ISO-8601 review checkpoint"), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - pr_sync(config: MergedKompassConfig, shell: Shell) { - const definition = createPrSyncTool(shell); - - return tool({ - description: definition.description, - args: { - title: tool.schema.string().describe("PR title; required when creating or renaming a PR").optional(), - body: tool.schema.string().describe("PR body override").optional(), - description: tool.schema.string().describe("Short PR description rendered above checklist sections").optional(), - base: tool.schema.string().describe("Base branch to merge into").optional(), - head: tool.schema.string().describe("Head branch to use when creating a PR").optional(), - labels: tool.schema.array(tool.schema.string()).describe("Labels to add to the PR").optional(), - removeLabels: tool.schema.array(tool.schema.string()).describe("Labels to remove from an existing PR").optional(), - replaceLabels: tool.schema.array(tool.schema.string()).describe("Exact label set for the PR; an empty array clears all labels").optional(), - assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the PR").optional(), - checklists: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist section name"), - items: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist item name"), - completed: tool.schema.boolean().describe("Whether the item is completed"), - })).describe("Checklist items"), - })).describe("Checklist sections rendered as markdown").optional(), - draft: tool.schema.boolean().describe("Create as draft PR").optional(), - refUrl: tool.schema.string().describe("Optional PR URL to update").optional(), - commitId: tool.schema.string().describe("Commit SHA to anchor review comments to; omit unless sending review comments").optional(), - review: tool.schema.object({ - body: tool.schema.string().describe("Optional review summary body").optional(), - comments: tool.schema.array(tool.schema.object({ - path: tool.schema.string().describe("Changed file path"), - body: tool.schema.string().describe("Inline review comment body"), - line: tool.schema.number().int().describe("Ending line on the diff side"), - startLine: tool.schema.number().int().describe("Starting line for multi-line comments").optional(), - side: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the ending line").optional(), - startSide: tool.schema.enum(["LEFT", "RIGHT"]).describe("Diff side for the starting line").optional(), - })).describe("Inline review comments to submit").optional(), - ...(config.shared.prApprove - ? { approve: tool.schema.boolean().describe("Approve the PR with this review comment").optional() } - : {} - ), - }).describe("Optional structured review submission; omit the field entirely unless submitting a review body, inline comments, or approval").optional(), - replies: tool.schema.array(tool.schema.object({ - inReplyTo: tool.schema.number().int().describe("Existing review comment ID to reply to"), - body: tool.schema.string().describe("Reply body"), - })).describe("Replies to existing review comments").optional(), - commentBody: tool.schema.string().describe("General PR comment body").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - ticket_sync(_: MergedKompassConfig, shell: Shell) { - const definition = createTicketSyncTool(shell); - return tool({ - description: definition.description, - args: { - title: tool.schema.string().describe("Issue title").optional(), - body: tool.schema.string().describe("Issue body override").optional(), - description: tool.schema.string().describe("Issue description rendered above checklist sections").optional(), - labels: tool.schema.array(tool.schema.string()).describe("Labels to apply to the issue").optional(), - assignees: tool.schema.array(tool.schema.string()).describe("Assignees to apply to the issue").optional(), - checklists: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist section name"), - items: tool.schema.array(tool.schema.object({ - name: tool.schema.string().describe("Checklist item name"), - completed: tool.schema.boolean().describe("Whether the item is completed"), - })).describe("Checklist items"), - })).describe("Checklist sections rendered as markdown").optional(), - refUrl: tool.schema.string().describe("Optional issue URL to update").optional(), - comments: tool.schema.array(tool.schema.string()).describe("Optional issue comments to post").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, - ticket_load(_: MergedKompassConfig, shell: Shell) { - const definition = createTicketLoadTool(shell); - return tool({ - description: definition.description, - args: { - source: tool.schema.string().describe("Issue URL, repo#id, #id, file path, or raw text"), - comments: tool.schema.boolean().describe("Include issue comments").optional(), - }, - execute: (args, context) => definition.execute(args, context), - }); - }, -}; - -export function createCoreOpenCodeToolDefinitions( - config: MergedKompassConfig, - projectRoot: string, -): Record { - const shell = createNodeShell(projectRoot); - return Object.fromEntries( - Object.entries(opencodeToolCreators).map(([name, create]) => [name, create(config, shell)]), - ); -} - export async function createOpenCodeTools( client: PluginInput["client"], projectRoot: string, diff --git a/packages/opencode/v2.ts b/packages/opencode/v2.ts index be67134..da09a3f 100644 --- a/packages/opencode/v2.ts +++ b/packages/opencode/v2.ts @@ -14,8 +14,8 @@ import { loadResolvedCommands, } from "./cache.ts"; import { createPluginLogger } from "./logging.ts"; +import { createCoreOpenCodeToolDefinitions } from "./shared-tools.ts"; import { getConfiguredOpenCodeToolName } from "./tool-names.ts"; -import { createCoreOpenCodeToolDefinitions } from "./v1.ts"; type PluginContext = Plugin.Context; type ToolDraft = Parameters[0]>[0]; From 9ffde109dbf93774a09cac577b364e0b8cffc246 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Mon, 31 Aug 2026 13:34:14 -0300 Subject: [PATCH 4/7] fix: dispose v2 plugin registrations --- packages/opencode/test/v2-plugin.test.ts | 53 +++++++++++++++--------- packages/opencode/v2.ts | 13 ++++-- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/opencode/test/v2-plugin.test.ts b/packages/opencode/test/v2-plugin.test.ts index 6378dc3..9be38ba 100644 --- a/packages/opencode/test/v2-plugin.test.ts +++ b/packages/opencode/test/v2-plugin.test.ts @@ -30,6 +30,7 @@ describe("OpenCode v2 plugin", () => { const prompts: Array> = []; const switchedAgents: string[] = []; const createdSessions: Array> = []; + const disposed: string[] = []; const availableAgents = new Map([ ["worker", { id: "worker", permissions: [] as unknown[] }], ["planner", { id: "planner", permissions: [] as unknown[] }], @@ -38,31 +39,40 @@ describe("OpenCode v2 plugin", () => { const context = { location: { directory: process.cwd() }, agent: { - transform: async (transform: (draft: unknown) => void) => transform({ - get(name: string) { - return availableAgents.get(name); - }, - update(name: string, update: (agent: Record) => void) { - agents.push(name); - update(availableAgents.get(name) as Record); - }, - }), + transform: async (transform: (draft: unknown) => void) => { + transform({ + get(name: string) { + return availableAgents.get(name); + }, + update(name: string, update: (agent: Record) => void) { + agents.push(name); + update(availableAgents.get(name) as Record); + }, + }); + return { dispose: async () => { disposed.push("agent"); } }; + }, reload: async () => {}, }, command: { - transform: async (transform: (draft: unknown) => void) => transform({ - add(command: { name: string; execute(input: unknown): Promise }) { - commands.set(command.name, command); - }, - }), + transform: async (transform: (draft: unknown) => void) => { + transform({ + add(command: { name: string; execute(input: unknown): Promise }) { + commands.set(command.name, command); + }, + }); + return { dispose: async () => { disposed.push("command"); } }; + }, reload: async () => {}, }, tool: { - transform: async (transform: (draft: unknown) => void) => transform({ - add(tool: { name: string; input: unknown }) { - tools.set(tool.name, tool); - }, - }), + transform: async (transform: (draft: unknown) => void) => { + transform({ + add(tool: { name: string; input: unknown }) { + tools.set(tool.name, tool); + }, + }); + return { dispose: async () => { disposed.push("tool"); } }; + }, reload: async () => {}, }, session: { @@ -79,7 +89,7 @@ describe("OpenCode v2 plugin", () => { }, }; - await setupOpenCodeV2(context as never); + const cleanup = await setupOpenCodeV2(context as never); assert.ok(agents.includes("worker")); assert.ok(commands.has("dev")); assert.deepEqual([...tools.keys()].sort(), [ @@ -110,5 +120,8 @@ describe("OpenCode v2 plugin", () => { }); assert.deepEqual(switchedAgents, ["worker"]); assert.equal(prompts[1]?.sessionID, "session-1"); + + await cleanup(); + assert.deepEqual(disposed.sort(), ["agent", "command", "tool"]); }); }); diff --git a/packages/opencode/v2.ts b/packages/opencode/v2.ts index da09a3f..b007eac 100644 --- a/packages/opencode/v2.ts +++ b/packages/opencode/v2.ts @@ -154,12 +154,19 @@ export async function setupOpenCodeV2(ctx: PluginContext) { loadResolvedCommands(projectRoot), createOpenCodeV2Tools(projectRoot), ]); - await ctx.agent.transform((draft) => applyAgents(draft, agents)); - await ctx.command.transform((draft) => applyCommands(draft, commands, ctx, projectRoot)); - await ctx.tool.transform((draft) => { + const agentRegistration = await ctx.agent.transform((draft) => applyAgents(draft, agents)); + const commandRegistration = await ctx.command.transform((draft) => applyCommands(draft, commands, ctx, projectRoot)); + const toolRegistration = await ctx.tool.transform((draft) => { for (const tool of tools) draft.add(tool); }); logger.info("Initialized Kompass v2 plugin", { directory: projectRoot, tools: tools.length }); + return async () => { + await Promise.all([ + agentRegistration.dispose(), + commandRegistration.dispose(), + toolRegistration.dispose(), + ]); + }; } export const OpenCodeCompassPluginV2 = Plugin.define({ From 3c25c10f2c6bb5a253e88a60e79446934966b67c Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Wed, 9 Sep 2026 14:50:02 -0300 Subject: [PATCH 5/7] feat: add native Rift worktrees for OpenCode v2 Register Rift through the V2 worktree API, preserve Rift-owned storage, and share native backend code with V1. Handle snapshot discovery, naming collisions, and removal through the native strategy contract. Update the plugin dependency, tests, packaging, and docs. Include the pending V2 command simplification and missing-agent registration changes. --- README.md | 24 +- bun.lock | 392 +++++++++++++++++- packages/opencode/README.md | 24 +- packages/opencode/package.json | 2 +- packages/opencode/rift-workspace.ts | 17 +- packages/opencode/rift-worktree.ts | 105 +++++ packages/opencode/rift.ts | 18 + packages/opencode/scripts/build.ts | 3 +- packages/opencode/test/rift-worktree.test.ts | 169 ++++++++ .../opencode/test/tool-registration.test.ts | 2 +- packages/opencode/test/v2-plugin.test.ts | 69 +-- packages/opencode/v2.ts | 52 +-- .../content/docs/docs/adapters/opencode.mdx | 27 +- .../docs/docs/reference/tools/index.mdx | 2 +- .../docs/docs/reference/tools/navigator.mdx | 2 +- 15 files changed, 790 insertions(+), 118 deletions(-) create mode 100644 packages/opencode/rift-worktree.ts create mode 100644 packages/opencode/rift.ts create mode 100644 packages/opencode/test/rift-worktree.test.ts diff --git a/README.md b/README.md index 68ceb73..ad3259d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ For OpenCode 2, use the plural `plugins` key: } ``` -OpenCode 2 agent transforms are update-only. If an agent is renamed in Kompass config, the host must register that agent ID before Kompass setup runs. +OpenCode 1 handles command expansion natively. On OpenCode 2, the plugin inserts the raw input at `$ARGUMENTS`, or appends it when that placeholder is absent. Numbered placeholders and shell interpolation in custom command templates are not supported by the v2 adapter. OpenCode 2 agent transforms create missing agent IDs, including renamed Kompass agents. Project config is optional. To start from the published base config: @@ -62,9 +62,27 @@ Kompass loads the bundled base config, then optional home-directory overrides, t The recommended project override path is `.opencode/kompass.jsonc`. +## OpenCode 2 worktrees + +Kompass registers `rift` as a native OpenCode 2 worktree strategy using its bundled `rift-snapshot` dependency. It becomes the location's default strategy and works with OpenCode's worktree actions and `ctx.worktree.create`, `list`, `refresh`, and `remove`. This requires a recent V2 release with plugin worktree strategies and returned-directory support; the adapter targets `@opencode/plugin` beta `19381`. + +Load Kompass in OpenCode's `opencode.jsonc`; no destination setting is needed: + +```jsonc +{ + "plugins": ["@kompassdev/opencode"] +} +``` + +Rift creates a copy-on-write snapshot of the source checkout, including dependencies and uncommitted files. Rift controls storage using its default `.rifts//` layout beside the source root; OpenCode records the returned directory. The strategy uses OpenCode's suggested name, suffixing collisions in Rift's storage, but does not use OpenCode's destination override or `worktree.directory` setting. Rift's configured creation hooks run, followed by OpenCode's project setup. Rift cannot select a starting branch or ref. Use an explicit `strategy: "git"` when creating from a ref. + +Inventory includes sibling and nested snapshots. Removal requests force confirmation for uncommitted files and requires child snapshots to be removed first, even with force. Rift moves removed snapshots to its trash; its `gc` operation reclaims that storage. + +The last active strategy registration wins. Unloading Kompass restores the previous default; existing snapshots keep their Rift owner and need the plugin available for removal. Rift errors are surfaced without a Git fallback. The strategy uses Rift's native bindings, loaded on first use, and requires a supported copy-on-write filesystem. Its synchronous native calls cannot be interrupted mid-operation; cancellation is checked between calls. + ## Kompass Navigator -Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required to register Navigator safely. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer. +Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. Its eight tools have not been ported to the OpenCode 2 plugin API; V2 uses the native Rift worktree strategy described above. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. The V1 Navigator adapter polls the active-session API locally when using the V2 session protocol. It requires OpenCode `1.17.12` or newer. Configure Navigator and its limits with: @@ -90,7 +108,7 @@ Set `adapters.opencode.navigator.enabled` to `false` to disable all Navigator to Navigator accepts only sessions from the current OpenCode project and only worktrees returned by OpenCode. New sessions inherit the calling session's agent, model, and variant unless explicitly overridden. It rejects self-targeting lifecycle calls, arbitrary directories, unknown V2 agent overrides, main-checkout removal, unmanaged worktrees, and removal while a worktree has active sessions. `session_send` can switch the target session's agent or model before admitting a steered prompt when the detected OpenCode protocol supports it. `session_wait` defaults to `maxWaitMs`, caps requested timeouts at `maxWaitMs`, and treats `timeoutMs: 0` as an immediate snapshot. Navigator never force-removes or automatically cleans up resources after a partial failure. -When OpenCode exposes experimental workspace adapters, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. Navigator automatically uses that adapter for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees only when the experimental API or Rift adapter is unavailable. +On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. Navigator automatically uses that adapter for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees only when the experimental API or Rift adapter is unavailable. Kompass preserves the OpenCode workspace ID when it creates future sessions in Rift workspaces. Existing sessions without workspace identity are not migrated automatically. Rift workspaces are experimental OpenCode workspaces, not legacy Desktop sandboxes, and Kompass does not modify `project.sandboxes` or OpenCode's workspace database. diff --git a/bun.lock b/bun.lock index ecb659a..51f1122 100644 --- a/bun.lock +++ b/bun.lock @@ -20,8 +20,8 @@ "name": "@kompassdev/opencode", "version": "0.17.4", "dependencies": { - "@opencode-ai/plugin": "0.0.0-beta-18684", "@opencode-ai/sdk": "^1.18.7", + "@opencode/plugin": "0.0.0-beta-19381", "rift-snapshot": "^0.0.10", "zod": "^4.1.8", }, @@ -69,10 +69,52 @@ "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], + + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.69", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-vpsh9VWmQVC/nsdzf72F2yUMBOFT1aq+hoLseYV03zjVXVHkjqSNJskFRKPFxc3MRFrHNbSSmOwsbYE15u9ecw=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.15", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-login": "^3.972.77", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.77", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.82", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-znDkEOGXB8W3kG1LJUKP3foBZY/9qLM0eil/DxWXSp37XsdsRLQHE/d/OaCGGVgKpA6znR38h/+INk8do1FjiA=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.14", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/token-providers": "3.1116.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1057.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1057.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.44", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.46", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q=="], + "@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.10", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -87,6 +129,12 @@ "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.112", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.112" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ=="], + + "@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.112", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ=="], + "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], @@ -163,6 +211,8 @@ "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.38.3", "", { "dependencies": { "@expressive-code/core": "^0.38.3" } }, "sha512-dPK3+BVGTbTmGQGU3Fkj3jZ3OltWUAlxetMHI6limUGCWBCucZiwoZeFM/WmqQa71GyKRzhBT+iEov6kkz2xVA=="], + "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -215,6 +265,10 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@kompassdev/core": ["@kompassdev/core@workspace:packages/core"], @@ -237,18 +291,76 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@opencode-ai/ai": ["@opencode-ai/ai@0.0.0-beta-18684", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-beta-18684", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-gZTPELkm9aSv0cxWt9gGSdN9fecO9/GlplzUT4knrBD70vxQGnOYx7LGDUPZokW8Fzn7gjD824Qh9AIVjyb07g=="], + "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], + + "@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="], + + "@npmcli/config": ["@npmcli/config@10.8.1", "", { "dependencies": { "@npmcli/map-workspaces": "^5.0.0", "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^6.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "walk-up-path": "^4.0.0" } }, "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ=="], + + "@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="], + + "@npmcli/git": ["@npmcli/git@7.0.2", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg=="], + + "@npmcli/installed-package-contents": ["@npmcli/installed-package-contents@4.0.0", "", { "dependencies": { "npm-bundled": "^5.0.0", "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" } }, "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA=="], + + "@npmcli/map-workspaces": ["@npmcli/map-workspaces@5.0.3", "", { "dependencies": { "@npmcli/name-from-folder": "^4.0.0", "@npmcli/package-json": "^7.0.0", "glob": "^13.0.0", "minimatch": "^10.0.3" } }, "sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw=="], + + "@npmcli/metavuln-calculator": ["@npmcli/metavuln-calculator@9.0.3", "", { "dependencies": { "cacache": "^20.0.0", "json-parse-even-better-errors": "^5.0.0", "pacote": "^21.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5" } }, "sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg=="], + + "@npmcli/name-from-folder": ["@npmcli/name-from-folder@4.0.0", "", {}, "sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg=="], - "@opencode-ai/client": ["@opencode-ai/client@0.0.0-beta-18684", "", { "dependencies": { "@opencode-ai/protocol": "0.0.0-beta-18684", "@opencode-ai/schema": "0.0.0-beta-18684" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-Qjq07sTId3gZcqqMsPg+gtqU3PXneOWje+BSzBXepimAfPNqL0aIpH+em2Y4KL3lxEuJK3vdfyx8UXI8gzGRlQ=="], + "@npmcli/node-gyp": ["@npmcli/node-gyp@5.0.0", "", {}, "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ=="], - "@opencode-ai/plugin": ["@opencode-ai/plugin@0.0.0-beta-18684", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/ai": "0.0.0-beta-18684", "@opencode-ai/client": "0.0.0-beta-18684", "@opencode-ai/protocol": "0.0.0-beta-18684", "@opencode-ai/schema": "0.0.0-beta-18684", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode-ai/theme": "0.0.0-beta-18684", "@opentui/core": ">=0.5.9", "@opentui/solid": ">=0.5.9", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode-ai/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-OOgIRF563MsMROHrxXuJtdG5729PE1DhN4ASRW6OuH0KNjEbIkCrnOJmBI5by+JSbwTbHrsgKXhk18ee9MpRUg=="], + "@npmcli/package-json": ["@npmcli/package-json@7.0.5", "", { "dependencies": { "@npmcli/git": "^7.0.0", "glob": "^13.0.0", "hosted-git-info": "^9.0.0", "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", "spdx-expression-parse": "^4.0.0" } }, "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ=="], - "@opencode-ai/protocol": ["@opencode-ai/protocol@0.0.0-beta-18684", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-beta-18684", "effect": "4.0.0-rc.112" } }, "sha512-BXeauVOxxcam0zgOxJcpkmoQQTiqq3L6l3i8qF/Wsa3iPf7cA1SCM6+3r6iJ/9akckEEi9ZL85M3b8NhOcU85A=="], + "@npmcli/promise-spawn": ["@npmcli/promise-spawn@9.0.1", "", { "dependencies": { "which": "^6.0.0" } }, "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q=="], - "@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-beta-18684", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-aoQEG+KBSTAq7IaNVpAfXKNSvcBaRwpjPgPL0DuKoj39g013/NoR/rmbWtjxKkkczBqbWp/91zLGvXW+bIOotQ=="], + "@npmcli/query": ["@npmcli/query@5.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ=="], + + "@npmcli/redact": ["@npmcli/redact@4.0.0", "", {}, "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q=="], + + "@npmcli/run-script": ["@npmcli/run-script@10.0.4", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0" } }, "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg=="], "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.7", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-5txVELvCxzC3O/MYnRKdTvBN3TqQjE3PaEks3kDRA/moWzMSUhdm15xCth/Mz/NPYyGTn2u4IdScdV9vLTfLRg=="], + "@opencode/ai": ["@opencode/ai@0.0.0-beta-19381", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "0.0.0-beta-19381", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-22V30RSke8NUKWYuDNwkKnO1mpzO1ouIqpOq70lUYB5A1a/UCLHNvAS76/2B+rdiVl0Y8jIPMkZ1BdV6VdZ+SA=="], + + "@opencode/client": ["@opencode/client@0.0.0-beta-19381", "", { "dependencies": { "@opencode/protocol": "0.0.0-beta-19381", "@opencode/schema": "0.0.0-beta-19381" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-VIJstFeUC/XceLqQK5Mt3tHEl5OlsUF/M+wkhkhbLMpFx5F1J5pwc7jdMaXBJVMJzF6MFy233SzHH2vI8ALBjQ=="], + + "@opencode/plugin": ["@opencode/plugin@0.0.0-beta-19381", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "0.0.0-beta-19381", "@opencode/client": "0.0.0-beta-19381", "@opencode/protocol": "0.0.0-beta-19381", "@opencode/schema": "0.0.0-beta-19381", "@opencode/util": "0.0.0-beta-19381", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "0.0.0-beta-19381", "@opentui/core": ">=0.5.10", "@opentui/solid": ">=0.5.10", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-xs71eTix7Y16bob6PQvgW7XS5FqhKWeHnuChpMYh4gxhUG91MiuqwP/zOUxpny+JIYetslT/T76ua92mqMhBDg=="], + + "@opencode/protocol": ["@opencode/protocol@0.0.0-beta-19381", "", { "dependencies": { "@opencode/schema": "0.0.0-beta-19381", "effect": "4.0.0-rc.112" } }, "sha512-e1iHI2IUDiIuzMsVMU7I77xlzwwD+69r4xEdfVtr4FZQvNYIHv771rgFpBIjeuE1e/GtP6gx0wa/zvf7fAJo6w=="], + + "@opencode/schema": ["@opencode/schema@0.0.0-beta-19381", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-wrVyp4RcwMHglc+OIev/WeR1tUo1WfUkOKh8AbE/sY5NubnOa16zKDqrYC419ruU2x1A3H7octb1atNdwRtCBg=="], + + "@opencode/util": ["@opencode/util@0.0.0-beta-19381", "", { "dependencies": { "@effect/opentelemetry": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/platform-node-shared": "4.0.0-rc.112", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "7.0.6", "effect": "4.0.0-rc.112", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "pacote": "21.5.1" } }, "sha512-DEm0InxoUwHV7U8xE2gw8tmU+J5wOON3A2Ht/REWI5KWtLpMq2z88x4gmgvUo4Ac6+c0s9/KQB4+6201Vg/hRQ=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ=="], @@ -267,6 +379,34 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + + "@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="], + + "@redis/client": ["@redis/client@6.2.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q=="], + + "@redis/json": ["@redis/json@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-AFIUJ8Gj0DaaSBHYuSt8+O0oYWM+50OK1c0OmodB7XERIA8+BbyV3O4v76f9iccWasd1/7qjfZTpuzexUaZtrQ=="], + + "@redis/search": ["@redis/search@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-2vfOAOyYFE7UUw3sBBlkqqruBtOUS4HRY5MtW4hp83llrwvtrTE4r22CEqXddlV+54zkLxBE4nmsIJ/dpezQrQ=="], + + "@redis/time-series": ["@redis/time-series@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-kiYniph04dJOole+L359B6C9E+jYS2uDP7hca6Onj0xF38ZIpyxARO0Iq0W4ZRn1e8Q6vqW00QFZVSMRA/2Ijw=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="], @@ -333,12 +473,32 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="], + + "@sigstore/core": ["@sigstore/core@3.2.1", "", {}, "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g=="], + + "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.5.2", "", {}, "sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw=="], + + "@sigstore/sign": ["@sigstore/sign@4.1.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.2", "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.0", "@sigstore/protobuf-specs": "^0.5.0", "make-fetch-happen": "^15.0.4", "proc-log": "^6.1.0" } }, "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ=="], + + "@sigstore/tuf": ["@sigstore/tuf@4.0.2", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "tuf-js": "^4.1.0" } }, "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ=="], + + "@sigstore/verify": ["@sigstore/verify@3.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA=="], + "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], + "@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-nxu3SgmAw9JXT2CtkU0m/XNLWpP9MsaBx1zAGAypCbYj15tIFlmcYwpF+Oh18le83d+IM9PT7ENdXnE4C+d5mA=="], @@ -349,6 +509,10 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tufjs/canonical-json": ["@tufjs/canonical-json@2.0.0", "", {}, "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA=="], + + "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -373,6 +537,8 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="], @@ -391,6 +557,8 @@ "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="], + "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -429,7 +597,7 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], @@ -441,14 +609,20 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -465,6 +639,8 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], @@ -473,6 +649,10 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + + "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -485,6 +665,8 @@ "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], @@ -555,6 +737,8 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], @@ -585,6 +769,8 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + "expressive-code": ["expressive-code@0.38.3", "", { "dependencies": { "@expressive-code/core": "^0.38.3", "@expressive-code/plugin-frames": "^0.38.3", "@expressive-code/plugin-shiki": "^0.38.3", "@expressive-code/plugin-text-markers": "^0.38.3" } }, "sha512-COM04AiUotHCKJgWdn7NtW2lqu8OW8owAidMpkXt1qxrZ9Q2iC7+tok/1qIn2ocGnczvr9paIySgGnEwFeEQ8Q=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -609,6 +795,8 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="], @@ -621,12 +809,14 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], "google-logging-utils": ["google-logging-utils@1.2.0", "", {}, "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], "h3": ["h3@1.15.10", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg=="], @@ -671,6 +861,8 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], @@ -679,14 +871,24 @@ "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "ip-address": ["ip-address@10.7.0", "", {}, "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA=="], + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], @@ -715,18 +917,30 @@ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="], + "jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="], + + "just-diff-apply": ["just-diff-apply@5.5.0", "", {}, "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], @@ -735,6 +949,8 @@ "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], @@ -849,10 +1065,28 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "minipass-fetch": ["minipass-fetch@5.0.2", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "optionalDependencies": { "iconv-lite": "^0.7.2" } }, "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ=="], + + "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@2.0.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -865,6 +1099,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], @@ -875,12 +1111,30 @@ "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="], + + "npm-install-checks": ["npm-install-checks@8.0.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="], + + "npm-package-arg": ["npm-package-arg@13.0.2", "", { "dependencies": { "hosted-git-info": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^7.0.0" } }, "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA=="], + + "npm-packlist": ["npm-packlist@10.0.4", "", { "dependencies": { "ignore-walk": "^8.0.0", "proc-log": "^6.0.0" } }, "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng=="], + + "npm-pick-manifest": ["npm-pick-manifest@11.0.3", "", { "dependencies": { "npm-install-checks": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "npm-package-arg": "^13.0.0", "semver": "^7.3.5" } }, "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ=="], + + "npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], @@ -893,6 +1147,8 @@ "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], + "p-map": ["p-map@7.0.7", "", {}, "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ=="], + "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], @@ -901,8 +1157,12 @@ "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "pacote": ["pacote@21.5.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg=="], + "pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="], + "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], @@ -913,7 +1173,7 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], @@ -925,20 +1185,32 @@ "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + "postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="], "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + + "proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="], + + "promise-all-reject-late": ["promise-all-reject-late@1.0.1", "", {}, "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw=="], + + "promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="], + "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + "read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], @@ -949,6 +1221,8 @@ "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -1005,6 +1279,8 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -1019,18 +1295,34 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sigstore": ["sigstore@4.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.1", "@sigstore/tuf": "^4.0.2", "@sigstore/verify": "^3.1.1" } }, "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="], + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], + "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], + + "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1049,12 +1341,16 @@ "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -1063,6 +1359,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tuf-js": ["tuf-js@4.1.0", "", { "dependencies": { "@tufjs/models": "4.1.0", "debug": "^4.4.3", "make-fetch-happen": "^15.0.1" } }, "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], @@ -1077,6 +1375,8 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], + "undici": ["undici@8.10.2", "", {}, "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -1107,6 +1407,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], @@ -1151,6 +1453,8 @@ "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], @@ -1165,10 +1469,16 @@ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yaml-language-server": ["yaml-language-server@1.20.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA=="], @@ -1193,8 +1503,20 @@ "@astrojs/yaml2ts/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/client-cognito-identity/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@aws-sdk/credential-provider-cognito-identity/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@aws-sdk/credential-provider-sso/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@aws-sdk/token-providers/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + "@expressive-code/plugin-shiki/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -1203,10 +1525,20 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + "@npmcli/arborist/common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], + + "@npmcli/git/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "@npmcli/promise-spawn/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "@opencode/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1223,12 +1555,22 @@ "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "node-gyp/undici": ["undici@6.28.1", "", {}, "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA=="], + + "node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "sitemap/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -1251,6 +1593,8 @@ "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], @@ -1269,6 +1613,10 @@ "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@npmcli/git/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + + "@npmcli/promise-spawn/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + "boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -1277,6 +1625,16 @@ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + + "rimraf/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "sitemap/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], @@ -1345,10 +1703,16 @@ "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], + + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/packages/opencode/README.md b/packages/opencode/README.md index 68ceb73..ad3259d 100644 --- a/packages/opencode/README.md +++ b/packages/opencode/README.md @@ -45,7 +45,7 @@ For OpenCode 2, use the plural `plugins` key: } ``` -OpenCode 2 agent transforms are update-only. If an agent is renamed in Kompass config, the host must register that agent ID before Kompass setup runs. +OpenCode 1 handles command expansion natively. On OpenCode 2, the plugin inserts the raw input at `$ARGUMENTS`, or appends it when that placeholder is absent. Numbered placeholders and shell interpolation in custom command templates are not supported by the v2 adapter. OpenCode 2 agent transforms create missing agent IDs, including renamed Kompass agents. Project config is optional. To start from the published base config: @@ -62,9 +62,27 @@ Kompass loads the bundled base config, then optional home-directory overrides, t The recommended project override path is `.opencode/kompass.jsonc`. +## OpenCode 2 worktrees + +Kompass registers `rift` as a native OpenCode 2 worktree strategy using its bundled `rift-snapshot` dependency. It becomes the location's default strategy and works with OpenCode's worktree actions and `ctx.worktree.create`, `list`, `refresh`, and `remove`. This requires a recent V2 release with plugin worktree strategies and returned-directory support; the adapter targets `@opencode/plugin` beta `19381`. + +Load Kompass in OpenCode's `opencode.jsonc`; no destination setting is needed: + +```jsonc +{ + "plugins": ["@kompassdev/opencode"] +} +``` + +Rift creates a copy-on-write snapshot of the source checkout, including dependencies and uncommitted files. Rift controls storage using its default `.rifts//` layout beside the source root; OpenCode records the returned directory. The strategy uses OpenCode's suggested name, suffixing collisions in Rift's storage, but does not use OpenCode's destination override or `worktree.directory` setting. Rift's configured creation hooks run, followed by OpenCode's project setup. Rift cannot select a starting branch or ref. Use an explicit `strategy: "git"` when creating from a ref. + +Inventory includes sibling and nested snapshots. Removal requests force confirmation for uncommitted files and requires child snapshots to be removed first, even with force. Rift moves removed snapshots to its trash; its `gc` operation reclaims that storage. + +The last active strategy registration wins. Unloading Kompass restores the previous default; existing snapshots keep their Rift owner and need the plugin available for removal. Rift errors are surfaced without a Git fallback. The strategy uses Rift's native bindings, loaded on first use, and requires a supported copy-on-write filesystem. Its synchronous native calls cannot be interrupted mid-operation; cancellation is checked between calls. + ## Kompass Navigator -Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required to register Navigator safely. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer. +Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. Its eight tools have not been ported to the OpenCode 2 plugin API; V2 uses the native Rift worktree strategy described above. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. The V1 Navigator adapter polls the active-session API locally when using the V2 session protocol. It requires OpenCode `1.17.12` or newer. Configure Navigator and its limits with: @@ -90,7 +108,7 @@ Set `adapters.opencode.navigator.enabled` to `false` to disable all Navigator to Navigator accepts only sessions from the current OpenCode project and only worktrees returned by OpenCode. New sessions inherit the calling session's agent, model, and variant unless explicitly overridden. It rejects self-targeting lifecycle calls, arbitrary directories, unknown V2 agent overrides, main-checkout removal, unmanaged worktrees, and removal while a worktree has active sessions. `session_send` can switch the target session's agent or model before admitting a steered prompt when the detected OpenCode protocol supports it. `session_wait` defaults to `maxWaitMs`, caps requested timeouts at `maxWaitMs`, and treats `timeoutMs: 0` as an immediate snapshot. Navigator never force-removes or automatically cleans up resources after a partial failure. -When OpenCode exposes experimental workspace adapters, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. Navigator automatically uses that adapter for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees only when the experimental API or Rift adapter is unavailable. +On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. Navigator automatically uses that adapter for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees only when the experimental API or Rift adapter is unavailable. Kompass preserves the OpenCode workspace ID when it creates future sessions in Rift workspaces. Existing sessions without workspace identity are not migrated automatically. Rift workspaces are experimental OpenCode workspaces, not legacy Desktop sandboxes, and Kompass does not modify `project.sandboxes` or OpenCode's workspace database. diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 562d271..609ccb5 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -47,7 +47,7 @@ "url": "https://github.com/kompassdev/kompass/issues" }, "dependencies": { - "@opencode-ai/plugin": "0.0.0-beta-18684", + "@opencode/plugin": "0.0.0-beta-19381", "@opencode-ai/sdk": "^1.18.7", "rift-snapshot": "^0.0.10", "zod": "^4.1.8" diff --git a/packages/opencode/rift-workspace.ts b/packages/opencode/rift-workspace.ts index 69ec040..21e6249 100644 --- a/packages/opencode/rift-workspace.ts +++ b/packages/opencode/rift-workspace.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { createRiftSnapshot, loadRift, type RiftBackend } from "./rift.ts"; type WorkspaceInfo = { id: string; @@ -22,12 +23,7 @@ type WorkspaceAdapter = { target(config: WorkspaceInfo): WorkspaceTarget | Promise; }; -type RiftModule = { - init(options?: { at?: string }): null; - create(options?: { from?: string; name?: string; copyAll?: boolean; hooks?: boolean }): string; - remove(options?: { at?: string; all?: false }): void; - list(options?: { of?: string }): string[]; -}; +type RiftModule = Pick; type RiftAdapterOptions = { sourceDirectory: string; @@ -46,8 +42,6 @@ type Logger = { warn(message: string, extra?: Record): void; }; -const importModule = new Function("specifier", "return import(specifier)") as (specifier: string) => Promise; - function slugify(input: string) { return input .trim() @@ -138,12 +132,9 @@ export function createRiftWorkspaceAdapter(rift: RiftModule, options: RiftAdapte requireManagedDirectory(sourceDirectory, source, "creation source"); } const expected = requireManagedDirectory(sourceDirectory, config.directory, "creation target"); - rift.init({ at: source }); - const created = path.resolve(rift.create({ + const created = path.resolve(createRiftSnapshot(rift, { from: source, name: config.name, - // Use Rift's APFS full-clone path; the filtered walker can fail on protected macOS xattrs. - copyAll: true, })); if (created !== expected) { try { @@ -195,7 +186,7 @@ export async function registerRiftWorkspaceAdapter(input: ExperimentalWorkspaceI let rift: RiftModule; try { - rift = await importModule("rift-snapshot") as RiftModule; + rift = await loadRift(); } catch (error) { logger.info("Rift workspace adapter not registered", { reason: "Install rift-snapshot to enable the experimental Rift workspace adapter", diff --git a/packages/opencode/rift-worktree.ts b/packages/opencode/rift-worktree.ts new file mode 100644 index 0000000..c22bbce --- /dev/null +++ b/packages/opencode/rift-worktree.ts @@ -0,0 +1,105 @@ +import { Worktree } from "@opencode/plugin"; +import type { WorktreeDefinition } from "@opencode/plugin/promise/worktree"; +import { execFile } from "node:child_process"; +import { realpath } from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { createRiftSnapshot, loadRift, type RiftBackend } from "./rift.ts"; + +const execFileAsync = promisify(execFile); + +function isUninitialized(error: unknown) { + return error instanceof Error && "code" in error && error.code === "workspace_not_initialized"; +} + +export function createRiftWorktreeStrategy(load: () => Promise = loadRift): WorktreeDefinition { + async function backend(signal: AbortSignal) { + signal.throwIfAborted(); + const rift = await load(); + signal.throwIfAborted(); + return rift; + } + + return { + id: "rift", + async create(input, { signal }) { + if (input.branch !== undefined) { + throw new Worktree.OperationError({ + message: "Rift snapshots the source checkout and cannot select a starting branch or ref. Use the git strategy for a starting ref.", + }); + } + const rift = await backend(signal); + // Rift's native calls are synchronous. Check cancellation before entering them; + // once creation completes, return its path so OpenCode can record ownership. + const name = path.basename(input.directory); + for (let attempt = 1; ; attempt++) { + signal.throwIfAborted(); + try { + const directory = createRiftSnapshot(rift, { + from: input.sourceDirectory, + name: attempt === 1 ? name : `${name}-${attempt}`, + }); + return { directory: path.resolve(directory) }; + } catch (error) { + // OpenCode checks its suggested destination, but Rift owns a different + // storage directory. Resolve collisions reported by Rift there. + if (!(error instanceof Error) || !("code" in error)) throw error; + const targetsSource = error.code === "inside_source" && "path" in error + && typeof error.path === "string" && path.resolve(error.path) === path.resolve(input.sourceDirectory); + if ((error.code !== "already_exists" && !targetsSource) || attempt >= 100) throw error; + } + } + }, + async list(sourceDirectory, { signal }) { + const rift = await backend(signal); + let ancestors: string[]; + try { + ancestors = rift.ancestors({ of: sourceDirectory }); + } catch (error) { + if (isUninitialized(error)) return []; + throw error; + } + const root = await realpath(ancestors.at(-1) ?? sourceDirectory); + const entries: Array<{ directory: string; type: "root" | "worktree" }> = [{ directory: root, type: "root" }]; + const seen = new Set([root]); + // Rift lists immediate children; OpenCode needs the whole project inventory, + // including siblings and snapshots created from another snapshot. + for (const entry of entries) { + signal.throwIfAborted(); + for (const child of rift.list({ of: entry.directory })) { + const directory = await realpath(child).catch((error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") return undefined; + throw error; + }); + if (!directory) continue; + if (seen.has(directory)) continue; + seen.add(directory); + entries.push({ directory, type: "worktree" }); + } + } + return entries; + }, + async remove(input, { signal }) { + const rift = await backend(signal); + const directory = await realpath(input.directory); + signal.throwIfAborted(); + const parent = rift.ancestors({ of: directory })[0]; + if (!parent || !rift.list({ of: parent }).some((child) => path.resolve(child) === directory)) { + throw new Worktree.OperationError({ message: `Not a Rift snapshot root: ${directory}` }); + } + if (!input.force) { + const { stdout } = await execFileAsync("git", ["-C", directory, "status", "--porcelain", "--untracked-files=all"], { signal }); + if (stdout.trim()) { + throw new Worktree.OperationError({ message: "Rift snapshot has uncommitted changes.", forceRequired: true }); + } + } + signal.throwIfAborted(); + // Rift removes an entire subtree. Check after the async status call so a + // snapshot created while Git was running cannot be removed with its parent. + if (rift.list({ of: directory }).length) { + throw new Worktree.OperationError({ message: "Remove this Rift snapshot's child snapshots first." }); + } + rift.remove({ at: directory }); + }, + }; +} diff --git a/packages/opencode/rift.ts b/packages/opencode/rift.ts new file mode 100644 index 0000000..8eb4075 --- /dev/null +++ b/packages/opencode/rift.ts @@ -0,0 +1,18 @@ +import type * as Rift from "rift-snapshot"; + +export type RiftBackend = Pick & { + remove(options?: Rift.RemoveOptions & { all?: false }): void; +}; + +export function loadRift(): Promise { + return import("rift-snapshot"); +} + +export function createRiftSnapshot( + rift: Pick, + options: Rift.CreateOptions, +) { + rift.init({ at: options.from }); + // Keep dependencies and working-tree state. On APFS this uses the full clone path. + return rift.create({ ...options, copyAll: true }); +} diff --git a/packages/opencode/scripts/build.ts b/packages/opencode/scripts/build.ts index e250a71..ba15fc5 100644 --- a/packages/opencode/scripts/build.ts +++ b/packages/opencode/scripts/build.ts @@ -14,7 +14,8 @@ const packageReadme = path.join(packageRoot, "README.md"); const runtimeDirs = ["agents", "commands", "components"] as const; const bundleExternals = [ - "@opencode-ai/plugin", + "@opencode/plugin", + "rift-snapshot", "@opencode-ai/sdk/v2", ] as const; const bundleArgs = [ diff --git a/packages/opencode/test/rift-worktree.test.ts b/packages/opencode/test/rift-worktree.test.ts new file mode 100644 index 0000000..5d29922 --- /dev/null +++ b/packages/opencode/test/rift-worktree.test.ts @@ -0,0 +1,169 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { Worktree } from "@opencode/plugin"; +import { createRiftWorktreeStrategy } from "../rift-worktree.ts"; +import type { RiftBackend } from "../rift.ts"; + +const execFileAsync = promisify(execFile); +const context = () => ({ signal: new AbortController().signal }); + +function backend(overrides: Partial = {}): RiftBackend { + return { + init: () => null, + create: () => { throw new Error("Unexpected create"); }, + remove: () => { throw new Error("Unexpected remove"); }, + list: () => [], + ancestors: () => [], + ...overrides, + }; +} + +describe("Rift V2 worktree strategy", () => { + test("uses OpenCode's source and name while letting Rift choose the destination", async () => { + const calls: unknown[] = []; + const strategy = createRiftWorktreeStrategy(async () => backend({ + init(options) { calls.push(options); return null; }, + create(options) { calls.push(options); return "/snapshots/actual"; }, + })); + assert.deepEqual(await strategy.create({ sourceDirectory: "/source", directory: "/custom/task" }, context()), { + directory: "/snapshots/actual", + }); + assert.deepEqual(calls, [ + { at: "/source" }, + { from: "/source", name: "task", copyAll: true }, + ]); + }); + + test("suffixes name collisions in Rift's own storage", async () => { + const names: Array = []; + const strategy = createRiftWorktreeStrategy(async () => backend({ + create(options) { + names.push(options?.name); + if (names.length === 1) throw Object.assign(new Error("Snapshot exists"), { code: "already_exists" }); + return `/projects/.rifts/source/${options?.name}`; + }, + })); + assert.deepEqual(await strategy.create({ sourceDirectory: "/source", directory: "/suggested/task" }, context()), { + directory: "/projects/.rifts/source/task-2", + }); + assert.deepEqual(names, ["task", "task-2"]); + }); + + test("suffixes a snapshot name that would target the source snapshot itself", async () => { + const sourceDirectory = "/projects/.rifts/source/task"; + const strategy = createRiftWorktreeStrategy(async () => backend({ + create(options) { + if (options?.name === "task") { + throw Object.assign(new Error("Cannot copy into itself"), { code: "inside_source", path: sourceDirectory }); + } + return `/projects/.rifts/source/${options?.name}`; + }, + })); + assert.deepEqual(await strategy.create({ sourceDirectory, directory: "/suggested/task" }, context()), { + directory: "/projects/.rifts/source/task-2", + }); + }); + + test("rejects starting refs and cancelled operations before loading Rift", async () => { + const strategy = createRiftWorktreeStrategy(async () => { throw new Error("Must not load"); }); + await assert.rejects(strategy.create({ sourceDirectory: "/source", directory: "/task", branch: "main" }, context()), + (error) => error instanceof Worktree.OperationError && /starting branch or ref/.test(error.message)); + const cancelled = { signal: AbortSignal.abort() }; + await assert.rejects(strategy.create({ sourceDirectory: "/source", directory: "/task" }, cancelled), { name: "AbortError" }); + await assert.rejects(strategy.list("/source", cancelled), { name: "AbortError" }); + await assert.rejects(strategy.remove({ directory: "/task", force: true }, cancelled), { name: "AbortError" }); + }); + + test("propagates creation errors without retrying", async () => { + let attempts = 0; + const failure = new Error("copy-on-write unavailable"); + const strategy = createRiftWorktreeStrategy(async () => backend({ create() { attempts++; throw failure; } })); + await assert.rejects(strategy.create({ sourceDirectory: "/source", directory: "/task" }, context()), (error) => error === failure); + assert.equal(attempts, 1); + }); + + test("uninitialized roots have no inventory; registry errors are surfaced", async () => { + const strategy = createRiftWorktreeStrategy(async () => backend({ + ancestors() { throw Object.assign(new Error("Not initialized"), { code: "workspace_not_initialized" }); }, + })); + assert.deepEqual(await strategy.list("/source", context()), []); + const failure = new Error("registry damaged"); + const broken = createRiftWorktreeStrategy(async () => backend({ ancestors() { throw failure; } })); + await assert.rejects(broken.list("/source", context()), (error) => error === failure); + }); + + test("discovers siblings and descendants from a snapshot, skipping missing entries", async (t) => { + const root = await realpath(await mkdtemp(path.join(os.tmpdir(), "kompass-rift-list-"))); + t.after(() => rm(root, { recursive: true, force: true })); + const source = path.join(root, "source"); + const first = path.join(root, "first"); + const second = path.join(root, "second"); + const nested = path.join(root, "nested"); + await Promise.all([source, first, second, nested].map((directory) => mkdir(directory))); + const strategy = createRiftWorktreeStrategy(async () => backend({ + ancestors: () => [source], + list: ({ of } = {}) => of === source ? [first, second, first, path.join(root, "missing")] : of === first ? [nested] : [], + })); + assert.deepEqual(await strategy.list(first, context()), [ + { directory: source, type: "root" }, + { directory: first, type: "worktree" }, + { directory: second, type: "worktree" }, + { directory: nested, type: "worktree" }, + ]); + }); + + test("removes only a snapshot root and never cascades to children, even with force", async (t) => { + const root = await realpath(await mkdtemp(path.join(os.tmpdir(), "kompass-rift-remove-"))); + t.after(() => rm(root, { recursive: true, force: true })); + const child = path.join(root, "child"); + await mkdir(child); + const removed: string[] = []; + let children: string[] = []; + const strategy = createRiftWorktreeStrategy(async () => backend({ + ancestors: ({ of } = {}) => of === root ? [] : [root], + list: ({ of } = {}) => of === root ? [child] : children, + remove: (options) => { removed.push(options!.at!); }, + })); + await assert.rejects(strategy.remove({ directory: root, force: true }, context()), /Not a Rift snapshot root/); + const subdirectory = path.join(child, "subdirectory"); + await mkdir(subdirectory); + await assert.rejects(strategy.remove({ directory: subdirectory, force: true }, context()), /Not a Rift snapshot root/); + children = ["descendant"]; + await assert.rejects(strategy.remove({ directory: child, force: true }, context()), /child snapshots first/); + assert.deepEqual(removed, []); + children = []; + await strategy.remove({ directory: child, force: true }, context()); + assert.deepEqual(removed, [child]); + }); + + test("requests force for uncommitted files and allows a clean snapshot's Rift marker", async (t) => { + const root = await realpath(await mkdtemp(path.join(os.tmpdir(), "kompass-rift-dirty-"))); + t.after(() => rm(root, { recursive: true, force: true })); + const snapshot = path.join(root, "snapshot"); + await mkdir(snapshot); + await execFileAsync("git", ["init", snapshot]); + // Rift itself excludes its marker in the snapshot's Git metadata. + await writeFile(path.join(snapshot, ".git", "info", "exclude"), ".rift\n"); + await writeFile(path.join(snapshot, ".rift"), "rift-marker"); + await writeFile(path.join(snapshot, "user.txt"), "uncommitted"); + let removed = 0; + const strategy = createRiftWorktreeStrategy(async () => backend({ + ancestors: () => [root], + list: ({ of } = {}) => of === root ? [snapshot] : [], + remove: () => { removed++; }, + })); + await assert.rejects(strategy.remove({ directory: snapshot, force: false }, context()), + (error) => error instanceof Worktree.OperationError && error.forceRequired === true); + assert.equal(removed, 0); + await strategy.remove({ directory: snapshot, force: true }, context()); + assert.equal(removed, 1); + await rm(path.join(snapshot, "user.txt")); + await strategy.remove({ directory: snapshot, force: false }, context()); + assert.equal(removed, 2); + }); +}); diff --git a/packages/opencode/test/tool-registration.test.ts b/packages/opencode/test/tool-registration.test.ts index 0f81cb8..7b7aa8f 100644 --- a/packages/opencode/test/tool-registration.test.ts +++ b/packages/opencode/test/tool-registration.test.ts @@ -129,7 +129,7 @@ async function createTempGitRepo() { describe("createOpenCodeTools", () => { test("Rift workspace adapter maps OpenCode workspaces to Rift snapshots", async () => { - const calls: Array<{ name: string; options?: Record }> = []; + const calls: Array<{ name: string; options?: unknown }> = []; const sourceDirectory = path.join(os.tmpdir(), "kompass-rift-source"); const adapter = createRiftWorkspaceAdapter({ init: (options) => { diff --git a/packages/opencode/test/v2-plugin.test.ts b/packages/opencode/test/v2-plugin.test.ts index 9be38ba..70d7d62 100644 --- a/packages/opencode/test/v2-plugin.test.ts +++ b/packages/opencode/test/v2-plugin.test.ts @@ -5,7 +5,8 @@ import { OpenCodeCompassPluginV2, setupOpenCodeV2, } from "../index.ts"; -import { expandCommand } from "../v2.ts"; +import { loadResolvedCommands } from "../cache.ts"; +import type { WorktreeDefinition } from "@opencode/plugin/promise/worktree"; describe("OpenCode v2 plugin", () => { test("exports the v2 plugin definition", () => { @@ -13,40 +14,33 @@ describe("OpenCode v2 plugin", () => { assert.equal(OpenCodeCompassPluginV2.setup, setupOpenCodeV2); }); - test("inlines output from failing shell substitutions", async () => { - const prompt = await expandCommand( - "Result: !`printf stdout; printf stderr >&2; exit 1`", - "", - process.cwd(), - ); - - assert.equal(prompt, "Result: stdoutstderr"); - }); - test("registers location-scoped agents, commands, and tools", async () => { const agents: string[] = []; - const commands = new Map }>(); - const tools = new Map(); + type Command = { name: string; execute(input: { sessionID: string; prompt: { text: string; files?: unknown[] }; delivery: string }): Promise }; + const commands = new Map(); const prompts: Array> = []; const switchedAgents: string[] = []; const createdSessions: Array> = []; + const tools = new Map(); const disposed: string[] = []; - const availableAgents = new Map([ - ["worker", { id: "worker", permissions: [] as unknown[] }], - ["planner", { id: "planner", permissions: [] as unknown[] }], - ["reviewer", { id: "reviewer", permissions: [] as unknown[] }], - ]); + const worktrees: WorktreeDefinition[] = []; + const availableAgents = new Map>(); const context = { location: { directory: process.cwd() }, + worktree: { + transform: async (transform: (draft: { add(strategy: WorktreeDefinition): void }) => void) => { + transform({ add: (strategy) => { worktrees.push(strategy); } }); + return { dispose: async () => { disposed.push("worktree"); } }; + }, + }, agent: { transform: async (transform: (draft: unknown) => void) => { transform({ - get(name: string) { - return availableAgents.get(name); - }, update(name: string, update: (agent: Record) => void) { agents.push(name); - update(availableAgents.get(name) as Record); + const agent = availableAgents.get(name) ?? { id: name, permissions: [] }; + update(agent); + availableAgents.set(name, agent); }, }); return { dispose: async () => { disposed.push("agent"); } }; @@ -56,7 +50,7 @@ describe("OpenCode v2 plugin", () => { command: { transform: async (transform: (draft: unknown) => void) => { transform({ - add(command: { name: string; execute(input: unknown): Promise }) { + add(command: Command) { commands.set(command.name, command); }, }); @@ -92,6 +86,7 @@ describe("OpenCode v2 plugin", () => { const cleanup = await setupOpenCodeV2(context as never); assert.ok(agents.includes("worker")); assert.ok(commands.has("dev")); + assert.deepEqual(worktrees.map((strategy) => strategy.id), ["rift"]); assert.deepEqual([...tools.keys()].sort(), [ "kompass_changes_load", "kompass_pr_load", @@ -102,26 +97,32 @@ describe("OpenCode v2 plugin", () => { ]); assert.equal(typeof (tools.get("kompass_changes_load")?.input as any)?.["~standard"]?.validate, "function"); - await commands.get("dev")?.execute({ + const resolved = await loadResolvedCommands(process.cwd()); + assert.equal(commands.size, Object.keys(resolved).length); + const text = "Keep literal $& $$ $1 $ARGUMENTS and !`printf should-not-run`"; + const files = [{ uri: "file:///project/example.ts" }]; + await commands.get("dev")!.execute({ sessionID: "session-1", - prompt: { text: "fix auth" }, - delivery: "steer", + prompt: { text, files }, + delivery: "queue", }); - assert.equal(createdSessions.length, 1); - assert.equal(createdSessions[0]?.agent, "worker"); - assert.equal(switchedAgents.length, 0); - assert.equal(prompts[0]?.sessionID, "child-1"); - assert.match(String(prompts[0]?.text), /fix auth/); + assert.equal(prompts[0]?.text, resolved.dev!.template.replaceAll("$ARGUMENTS", () => text)); + assert.equal(prompts[0]?.files, files); + assert.equal(prompts[0]?.delivery, "queue"); + assert.equal(prompts[0]?.sessionID, process.env.CI ? "session-1" : "child-1"); + assert.equal(createdSessions.length, process.env.CI ? 0 : 1); + if (!process.env.CI) assert.equal(createdSessions[0]?.agent, "worker"); - await commands.get("learn")?.execute({ + await commands.get("learn")!.execute({ sessionID: "session-1", prompt: { text: "remember this" }, delivery: "steer", }); - assert.deepEqual(switchedAgents, ["worker"]); + assert.equal(switchedAgents.at(-1), "worker"); assert.equal(prompts[1]?.sessionID, "session-1"); + assert.equal(prompts[1]?.text, resolved.learn!.template.replaceAll("$ARGUMENTS", "remember this")); await cleanup(); - assert.deepEqual(disposed.sort(), ["agent", "command", "tool"]); + assert.deepEqual(disposed.sort(), ["agent", "command", "tool", "worktree"]); }); }); diff --git a/packages/opencode/v2.ts b/packages/opencode/v2.ts index b007eac..4edc2f2 100644 --- a/packages/opencode/v2.ts +++ b/packages/opencode/v2.ts @@ -1,6 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; +import { Plugin } from "@opencode/plugin"; import { z } from "zod"; import { @@ -16,11 +14,11 @@ import { import { createPluginLogger } from "./logging.ts"; import { createCoreOpenCodeToolDefinitions } from "./shared-tools.ts"; import { getConfiguredOpenCodeToolName } from "./tool-names.ts"; +import { createRiftWorktreeStrategy } from "./rift-worktree.ts"; type PluginContext = Plugin.Context; type ToolDraft = Parameters[0]>[0]; type OpenCodeTool = Parameters[0]; -const execFileAsync = promisify(execFile); function toOpenCodeTool( name: string, @@ -60,9 +58,6 @@ function applyAgents( agents: Record, ) { for (const [name, definition] of Object.entries(agents)) { - if (!draft.get(name)) { - throw new Error(`OpenCode v2 agent "${name}" must be registered before Kompass setup`); - } draft.update(name, (agent) => { agent.description = definition.description; agent.permissions.push(...Object.entries(definition.permission).map(([action, effect]) => ({ @@ -76,41 +71,6 @@ function applyAgents( } } -const argumentPattern = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi; -const placeholderPattern = /\$(\d+)/g; -const shellPattern = /!`([^`]+)`/g; - -export async function expandCommand(template: string, input: string, projectRoot: string) { - const args = (input.match(argumentPattern) ?? []).map((arg) => arg.replace(/^['"]|['"]$/g, "")); - const placeholders = template.match(placeholderPattern) ?? []; - const last = Math.max(0, ...placeholders.map((item) => Number(item.slice(1)))); - const expanded = template.replaceAll(placeholderPattern, (_, rawIndex: string) => { - const index = Number(rawIndex) - 1; - if (index >= args.length) return ""; - return index + 1 === last ? args.slice(index).join(" ") : args[index] ?? ""; - }); - const withArguments = expanded.replaceAll("$ARGUMENTS", input); - const text = placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim() - ? `${withArguments}\n\n${input}`.trim() - : withArguments.trim(); - const matches = Array.from(text.matchAll(shellPattern)); - const outputs = await Promise.all(matches.map(async (match) => { - try { - const { stdout, stderr } = await execFileAsync("/bin/bash", ["-lc", match[1] ?? ""], { - cwd: projectRoot, - encoding: "utf8", - maxBuffer: 10 * 1024 * 1024, - }); - return `${stdout}${stderr}`; - } catch (error) { - const failed = error as { stdout?: unknown; stderr?: unknown; message?: unknown }; - return `${String(failed.stdout ?? "")}${String(failed.stderr ?? failed.message ?? "")}`; - } - })); - const iterator = outputs[Symbol.iterator](); - return text.replace(shellPattern, () => iterator.next().value ?? ""); -} - function applyCommands( draft: Parameters[0]>[0], commands: Record, @@ -138,7 +98,9 @@ function applyCommands( await ctx.session.prompt({ ...prompt, sessionID: targetSessionID, - text: await expandCommand(definition.template, prompt.text, projectRoot), + text: definition.template.includes("$ARGUMENTS") + ? definition.template.replaceAll("$ARGUMENTS", () => prompt.text) + : `${definition.template}\n\n${prompt.text}`.trim(), delivery, }); }, @@ -159,12 +121,16 @@ export async function setupOpenCodeV2(ctx: PluginContext) { const toolRegistration = await ctx.tool.transform((draft) => { for (const tool of tools) draft.add(tool); }); + const worktreeRegistration = await ctx.worktree.transform((draft) => { + draft.add(createRiftWorktreeStrategy()); + }); logger.info("Initialized Kompass v2 plugin", { directory: projectRoot, tools: tools.length }); return async () => { await Promise.all([ agentRegistration.dispose(), commandRegistration.dispose(), toolRegistration.dispose(), + worktreeRegistration.dispose(), ]); }; } diff --git a/packages/web/src/content/docs/docs/adapters/opencode.mdx b/packages/web/src/content/docs/docs/adapters/opencode.mdx index 9979755..75bd347 100644 --- a/packages/web/src/content/docs/docs/adapters/opencode.mdx +++ b/packages/web/src/content/docs/docs/adapters/opencode.mdx @@ -9,7 +9,7 @@ The OpenCode adapter package is `@kompassdev/opencode`. It exports one definition for both plugin APIs. Configure it with `"plugin": ["@kompassdev/opencode"]` on OpenCode 1 or `"plugins": ["@kompassdev/opencode"]` on OpenCode 2. -OpenCode 2 agent transforms are update-only. If an agent is renamed in Kompass config, the host must register that agent ID before Kompass setup runs. +OpenCode 1 handles command expansion natively. On OpenCode 2, the plugin inserts the raw input at `$ARGUMENTS`, or appends it when that placeholder is absent. Numbered placeholders and shell interpolation in custom command templates are not supported by the v2 adapter. OpenCode 2 agent transforms create missing agent IDs, including renamed Kompass agents. ## What it provides @@ -19,6 +19,7 @@ Inside OpenCode, the adapter exposes: - Kompass agents - Kompass tools - Kompass Navigator session and worktree orchestration through the OpenCode 1 plugin API +- a native Rift worktree strategy for OpenCode 2 - project override loading from local config files ## Project overrides @@ -35,9 +36,29 @@ You can use overrides to: - adjust adapter behavior such as `agentMode` - enable and limit Navigator +## OpenCode 2 worktrees + +Kompass registers `rift` through V2's native `ctx.worktree.transform` API. Registration selects it as the location's default, so OpenCode's own worktree actions use Rift. The adapter targets `@opencode/plugin` beta `19381`, including support for a strategy's returned directory. + +Load Kompass in OpenCode's `opencode.jsonc`; no destination setting is needed: + +```jsonc +{ + "plugins": ["@kompassdev/opencode"] +} +``` + +- Snapshots preserve dependencies and uncommitted files through Rift's full copy-on-write path. Rift's creation hooks run, followed by OpenCode's project setup. +- Rift controls storage using its default `.rifts//` layout beside the source root. The strategy uses OpenCode's suggested name and suffixes collisions in Rift's storage, but does not use OpenCode's destination override or `worktree.directory` setting. OpenCode records the actual directory Rift returns. Inventory includes sibling and nested snapshots from Rift's registry. +- Starting branch/ref overrides are rejected because Rift snapshots the source checkout. Use `strategy: "git"` explicitly when creating from a ref. +- Removal requests force confirmation for uncommitted files. Child snapshots must be removed first, even with force, because Rift removal otherwise deletes an entire subtree. Removed snapshots go to Rift's trash; Rift's `gc` reclaims the storage. +- The last active strategy registration wins. Unloading Kompass restores the previous default. Existing snapshots retain their Rift owner and require the plugin for removal. Errors never trigger a Git fallback. + +The bundled `rift-snapshot` native bindings load on first use and require a supported copy-on-write filesystem. Native calls are synchronous and cannot be interrupted mid-operation; the adapter checks cancellation between calls. The V2 strategy is independent of the V1 Navigator settings. + ## Kompass Navigator -Navigator is available through the OpenCode 1 plugin API for explicitly requested native-session and managed-worktree workflows. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required to register Navigator safely. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. Navigator follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on the same compatible API in the current checkout or managed Git worktrees. Until OpenCode implements V2 wait, Navigator waits by polling the active-session API locally. It requires OpenCode `1.17.12` or newer and is enabled by default on OpenCode 1. +Navigator is available through the OpenCode 1 plugin API for explicitly requested native-session and managed-worktree workflows. Its eight tools have not been ported to the OpenCode 2 plugin API; V2 uses the native Rift strategy above. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. Navigator follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on the same compatible API in the current checkout or managed Git worktrees. The V1 Navigator adapter polls the active-session API locally when using the V2 session protocol. It requires OpenCode `1.17.12` or newer and is enabled by default on OpenCode 1. ```jsonc { @@ -63,7 +84,7 @@ Navigator registers all eight logical tools unless the feature or an individual Navigator is scoped to the current project. New sessions inherit the calling session's agent, model, and variant unless explicitly overridden. Session IDs are ownership-checked, directories must be native managed worktrees, and lifecycle calls cannot target the calling session. `session_send` can switch the target session's agent or model before admitting a steered prompt. Waits default to `maxWaitMs`, cap requested timeouts at `maxWaitMs`, and use `timeoutMs: 0` for an immediate snapshot. Removal rejects the checkout, unmanaged worktrees, and worktrees containing active sessions. Navigator does not force removal, merge branches, create pull requests, or maintain a separate job database. -When OpenCode exposes experimental workspace adapters, Kompass registers a `rift` workspace adapter for OpenCode's workspace API using its bundled `rift-snapshot` dependency. Navigator automatically uses Rift for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees otherwise. +On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter for OpenCode's workspace API using its bundled `rift-snapshot` dependency. Navigator automatically uses Rift for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees otherwise. ## Agent model diff --git a/packages/web/src/content/docs/docs/reference/tools/index.mdx b/packages/web/src/content/docs/docs/reference/tools/index.mdx index c08db72..0d5bcf2 100644 --- a/packages/web/src/content/docs/docs/reference/tools/index.mdx +++ b/packages/web/src/content/docs/docs/reference/tools/index.mdx @@ -50,7 +50,7 @@ Creates or updates GitHub issues, renders checklist sections, and can append iss ## OpenCode Navigator tools -Navigator tools are enabled by default through the OpenCode 1 plugin API. OpenCode 2 does not yet expose the host APIs they require. Use them only for explicitly requested native-session or managed-worktree workflows, not for subagent delegation; OpenCode's built-in `task` tool owns subagent lifecycle and waiting. Their default runtime names use the `kompass_` prefix. See [Kompass Navigator](./navigator/) for arguments and safety behavior. +Navigator tools are enabled by default through the OpenCode 1 plugin API. These tools have not been ported to the V2 plugin API; OpenCode 2 uses Kompass's [native Rift worktree strategy](/docs/adapters/opencode/#opencode-2-worktrees). Use them only for explicitly requested native-session or managed-worktree workflows, not for subagent delegation; OpenCode's built-in `task` tool owns subagent lifecycle and waiting. Their default runtime names use the `kompass_` prefix. See [Kompass Navigator](./navigator/) for arguments and safety behavior. ### `worktree_list` diff --git a/packages/web/src/content/docs/docs/reference/tools/navigator.mdx b/packages/web/src/content/docs/docs/reference/tools/navigator.mdx index a1ee198..32e87dd 100644 --- a/packages/web/src/content/docs/docs/reference/tools/navigator.mdx +++ b/packages/web/src/content/docs/docs/reference/tools/navigator.mdx @@ -3,7 +3,7 @@ title: Kompass Navigator description: Native OpenCode session and managed-worktree orchestration tools. --- -Navigator is enabled by default through the OpenCode 1 plugin API. OpenCode 2 does not yet expose the workspace, worktree, and session-discovery APIs required by these tools. Use Navigator only when the user explicitly requests native-session, managed-worktree, or multi-session workflow orchestration. Do not use it for subagent delegation: OpenCode's built-in `task` tool creates child sessions, waits for foreground tasks, and reports background completion automatically. Default runtime names use the `kompass_` prefix; set `adapters.opencode.navigator.enabled` to `false` to disable all eight tools. +Navigator is enabled by default through the OpenCode 1 plugin API. These eight tools have not been ported to the V2 plugin API; OpenCode 2 uses Kompass's [native Rift worktree strategy](/docs/adapters/opencode/#opencode-2-worktrees). Use Navigator only when the user explicitly requests native-session, managed-worktree, or multi-session workflow orchestration. Do not use it for subagent delegation: OpenCode's built-in `task` tool creates child sessions, waits for foreground tasks, and reports background completion automatically. Default runtime names use the `kompass_` prefix; set `adapters.opencode.navigator.enabled` to `false` to disable all eight tools. | Logical tool | Purpose | | --- | --- | From 220dfd376e1f67b7146e228e1d2435550405ba20 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Wed, 9 Sep 2026 16:57:52 -0300 Subject: [PATCH 6/7] feat: add native Effect session orchestration for OpenCode v2 Use native Effect registrations, commands, tools, and Rift operations. Add independent worktree creation and removal, and simplify session creation to optional directory and prompt inputs on both adapters. Update configuration, generated output, documentation, and lifecycle tests. --- README.md | 36 +- bun.lock | 2 + kompass.jsonc | 5 +- kompass.schema.json | 14 +- packages/core/index.ts | 1 + packages/core/kompass.jsonc | 5 +- packages/core/lib/config.ts | 19 + packages/opencode/.opencode/kompass.jsonc | 3 + packages/opencode/README.md | 36 +- packages/opencode/index.ts | 2 +- packages/opencode/kompass.jsonc | 5 +- packages/opencode/navigator-v2.ts | 252 ++++++++++++++ packages/opencode/navigator.ts | 179 +++------- packages/opencode/package.json | 2 + packages/opencode/rift-worktree.ts | 145 ++++---- packages/opencode/scripts/build.ts | 2 + packages/opencode/shared-tools.ts | 39 ++- packages/opencode/test/navigator-v2.test.ts | 325 ++++++++++++++++++ packages/opencode/test/navigator.test.ts | 112 +++--- packages/opencode/test/plugin-entry.test.ts | 4 +- packages/opencode/test/rift-worktree.test.ts | 93 ++--- packages/opencode/test/v2-plugin.test.ts | 218 ++++++------ packages/opencode/v2.ts | 130 +++---- .../web/src/components/CommandShowcase.astro | 2 +- .../content/docs/docs/adapters/opencode.mdx | 36 +- .../src/content/docs/docs/config/schema.mdx | 4 +- .../docs/docs/reference/tools/index.mdx | 21 +- .../docs/docs/reference/tools/navigator.mdx | 80 ++++- packages/web/src/pages/index.astro | 2 +- 29 files changed, 1229 insertions(+), 545 deletions(-) create mode 100644 packages/opencode/navigator-v2.ts create mode 100644 packages/opencode/test/navigator-v2.test.ts diff --git a/README.md b/README.md index ad3259d..2aee792 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl - Commands cover direct work (`/ask`, `/commit`, `/merge`, `/skill/create`, `/skill/optimize`), orchestration (`/dev`, `/ship`, `/todo`, `/pr/fix/loop`), ticket planning/sync, and PR review/shipping flows. `/branch/inline`, `/commit/inline`, `/commit-and-push/inline`, `/pr/create/inline`, and `/ship/inline` reuse the invoking session instead of starting a subtask. - Agents are intentionally narrow: `worker` handles implementation and multi-step workflows, `planner` is no-edit planning, and `reviewer` is a no-edit review specialist. -- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. OpenCode 1 also includes the eight Navigator session and worktree tools. +- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. Navigator adds nine session/worktree tools on OpenCode 1 and eight native orchestration tools on OpenCode 2. - Reusable command-template components live in `packages/core/components/` and are documented in the components reference. ## Prerequisites @@ -64,7 +64,7 @@ The recommended project override path is `.opencode/kompass.jsonc`. ## OpenCode 2 worktrees -Kompass registers `rift` as a native OpenCode 2 worktree strategy using its bundled `rift-snapshot` dependency. It becomes the location's default strategy and works with OpenCode's worktree actions and `ctx.worktree.create`, `list`, `refresh`, and `remove`. This requires a recent V2 release with plugin worktree strategies and returned-directory support; the adapter targets `@opencode/plugin` beta `19381`. +Kompass registers `rift` as a native OpenCode 2 worktree strategy using its bundled `rift-snapshot` dependency. It becomes the location's default for API calls that omit a strategy. Desktop's current New worktree action explicitly selects Git, so that button does not use Rift; Kompass's `worktree_create` tool uses the registered default. This requires a recent V2 release with plugin worktree strategies and returned-directory support; the adapter targets `@opencode/plugin` beta `19381`. Load Kompass in OpenCode's `opencode.jsonc`; no destination setting is needed: @@ -80,9 +80,35 @@ Inventory includes sibling and nested snapshots. Removal requests force confirma The last active strategy registration wins. Unloading Kompass restores the previous default; existing snapshots keep their Rift owner and need the plugin available for removal. Rift errors are surfaced without a Git fallback. The strategy uses Rift's native bindings, loaded on first use, and requires a supported copy-on-write filesystem. Its synchronous native calls cannot be interrupted mid-operation; cancellation is checked between calls. -## Kompass Navigator +## OpenCode 2 session orchestration -Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. Its eight tools have not been ported to the OpenCode 2 plugin API; V2 uses the native Rift worktree strategy described above. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. The V1 Navigator adapter polls the active-session API locally when using the V2 session protocol. It requires OpenCode `1.17.12` or newer. +Worktrees and sessions have separate lifecycles. `kompass_worktree_create({ name?, strategy?, branch? })` creates a worktree without a session. Pass its returned `directory` to `session_create`. Omit `directory` to use the calling session's current location, including when it is already in a worktree. Omit `prompt` to create an idle session, then start it with `session_send`. With a prompt, creation starts execution immediately. `session_send` targets a session ID and uses that session's location. + +`kompass_worktree_remove({ directory, force? })` removes a managed worktree through OpenCode. It rejects the checkout and any worktree containing the calling session or plugin. `force` defaults to false; true permits discarding uncommitted files. Rift still rejects snapshots with children. V2 cannot discover other active sessions, so finish or interrupt known sessions in that directory before removal. Removing a worktree does not delete its sessions. + +Navigator is enabled by default on V2 and exposes `kompass_worktree_create`, `kompass_worktree_list`, `kompass_worktree_remove`, `kompass_session_create`, `kompass_session_read`, `kompass_session_send`, `kompass_session_wait`, and `kompass_session_interrupt`. Use these for explicitly requested native-session workflows. Ordinary delegation uses OpenCode's built-in subagent tool. + +For example, call `kompass_worktree_create` with `{ "name": "parser-fix" }`, then pass the returned directory to `kompass_session_create`: + +```json +{ + "prompt": "Implement the parser fix and run its tests.", + "title": "Parser fix", + "directory": "/projects/.rifts/repo/parser-fix" +} +``` + +Session creation inherits the caller's agent/model/variant unless overridden, admits the optional first prompt, and returns the session ID and directory immediately. Explicit directories may select the project checkout or a managed worktree; relative paths resolve from the caller's directory. Session creation never creates or removes worktrees. V2 model overrides use `{ "providerID": "...", "id": "...", "variant": "..." }`. On `worktree_create`, a starting `branch` requires `strategy: "git"`; Rift uses the source checkout's state. + +Use the returned session ID to send steered or queued follow-ups, read bounded current context, wait for completion, or interrupt execution. Waits use the native Effect API, support one to eight targets, and release wait subscriptions on timeout or cancellation without interrupting the target sessions. `timeoutMs` must be positive and is capped by `maxWaitMs`. Reads use the current model context, not paginated full history. Partial failures report known resources and preserve them for recovery. + +The existing `adapters.opencode.navigator.enabled`, tool aliases/disables, `maxReadChars`, `maxOutputCharsPerItem`, and `maxWaitMs` settings apply to V2. `session_list` and the `maxConcurrentSessions` limit remain V1-only because the V2 plugin context lacks session discovery. Known session IDs continue to work after plugin reloads; Kompass does not keep its own session registry. + +## OpenCode 1 Navigator + +V1 also uses `session_create({ directory?, prompt?, agent?, model? })`: omit directory for the calling session's location, or pass the directory from a separate `worktree_create` call. The old `environment` argument has been removed. Omit prompt for an idle session. On the legacy session protocol, agent/model selection happens only when prompting, so supply those overrides to `session_send` when starting an idle session. V1 model overrides use `modelID`; V2 uses `id`. + +Navigator's OpenCode 1 adapter provides nine tools, including standalone `worktree_create({ name?, startCommand? })`. It prefers Rift when available; `startCommand` selects Git. V1 removal checks active sessions and has no force option. Ordinary delegation uses OpenCode's built-in `task` tool. Navigator follows Desktop's protocol detection and requires OpenCode `1.17.12` or newer. Configure Navigator and its limits with: @@ -108,7 +134,7 @@ Set `adapters.opencode.navigator.enabled` to `false` to disable all Navigator to Navigator accepts only sessions from the current OpenCode project and only worktrees returned by OpenCode. New sessions inherit the calling session's agent, model, and variant unless explicitly overridden. It rejects self-targeting lifecycle calls, arbitrary directories, unknown V2 agent overrides, main-checkout removal, unmanaged worktrees, and removal while a worktree has active sessions. `session_send` can switch the target session's agent or model before admitting a steered prompt when the detected OpenCode protocol supports it. `session_wait` defaults to `maxWaitMs`, caps requested timeouts at `maxWaitMs`, and treats `timeoutMs: 0` as an immediate snapshot. Navigator never force-removes or automatically cleans up resources after a partial failure. -On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. Navigator automatically uses that adapter for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees only when the experimental API or Rift adapter is unavailable. +On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. `worktree_create` uses that adapter when no `startCommand` is requested, using Git worktrees otherwise. Pass its returned directory to `session_create`. Kompass preserves the OpenCode workspace ID when it creates future sessions in Rift workspaces. Existing sessions without workspace identity are not migrated automatically. Rift workspaces are experimental OpenCode workspaces, not legacy Desktop sandboxes, and Kompass does not modify `project.sandboxes` or OpenCode's workspace database. diff --git a/bun.lock b/bun.lock index 51f1122..52b4ebd 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,8 @@ "dependencies": { "@opencode-ai/sdk": "^1.18.7", "@opencode/plugin": "0.0.0-beta-19381", + "@opencode/schema": "0.0.0-beta-19381", + "effect": "4.0.0-rc.112", "rift-snapshot": "^0.0.10", "zod": "^4.1.8", }, diff --git a/kompass.jsonc b/kompass.jsonc index 047f44a..fddc71c 100644 --- a/kompass.jsonc +++ b/kompass.jsonc @@ -54,8 +54,9 @@ "ticket_load": { "enabled": true }, "ticket_sync": { "enabled": true }, "worktree_list": { "enabled": true }, + "worktree_create": { "enabled": true }, "session_create": { "enabled": true }, - "session_list": { "enabled": true }, + "session_list": { "enabled": true }, // OpenCode 1 only "session_read": { "enabled": true }, "session_send": { "enabled": true }, "session_wait": { "enabled": true }, @@ -88,7 +89,7 @@ "agentMode": "all", "navigator": { "enabled": true, - "maxConcurrentSessions": 8, + "maxConcurrentSessions": 8, // OpenCode 1 only; V2 plugins lack active-session discovery "maxReadChars": 20000, "maxOutputCharsPerItem": 4000, "maxWaitMs": 120000, diff --git a/kompass.schema.json b/kompass.schema.json index 0efd73f..8fc296c 100644 --- a/kompass.schema.json +++ b/kompass.schema.json @@ -239,11 +239,15 @@ "worktree_list": { "$ref": "#/$defs/toolConfig" }, + "worktree_create": { + "$ref": "#/$defs/toolConfig" + }, "session_create": { "$ref": "#/$defs/toolConfig" }, "session_list": { - "$ref": "#/$defs/toolConfig" + "$ref": "#/$defs/toolConfig", + "description": "OpenCode 1 Navigator only; V2 plugins do not expose session listing." }, "session_read": { "$ref": "#/$defs/toolConfig" @@ -258,7 +262,8 @@ "$ref": "#/$defs/toolConfig" }, "worktree_remove": { - "$ref": "#/$defs/toolConfig" + "$ref": "#/$defs/toolConfig", + "description": "Remove managed worktrees. V2 uses native removal with optional force and cannot discover active sessions." } } }, @@ -356,8 +361,8 @@ "type": "object", "additionalProperties": false, "properties": { - "enabled": { "type": "boolean", "default": true }, - "maxConcurrentSessions": { "type": "integer", "minimum": 1, "maximum": 8, "default": 8 }, + "enabled": { "type": "boolean", "default": true, "description": "Enable Navigator: nine tools on OpenCode 1 and eight native orchestration tools on OpenCode 2." }, + "maxConcurrentSessions": { "type": "integer", "minimum": 1, "maximum": 8, "default": 8, "description": "OpenCode 1 only. The V2 plugin API does not expose active-session discovery, so V2 does not enforce this limit." }, "maxReadChars": { "type": "integer", "minimum": 1, "default": 20000 }, "maxOutputCharsPerItem": { "type": "integer", "minimum": 1, "default": 4000 }, "maxWaitMs": { "type": "integer", "minimum": 1, "default": 120000 } @@ -469,6 +474,7 @@ "ticket_sync", "ticket_load", "worktree_list", + "worktree_create", "session_create", "session_list", "session_read", diff --git a/packages/core/index.ts b/packages/core/index.ts index 5ae9986..662f7b9 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -9,6 +9,7 @@ export { getConfiguredToolName, getEnabledToolNames, NAVIGATOR_TOOL_NAMES, + V2_NAVIGATOR_TOOL_NAMES, loadKompassConfig, mergeWithDefaults, } from "./lib/config.ts"; diff --git a/packages/core/kompass.jsonc b/packages/core/kompass.jsonc index 047f44a..fddc71c 100644 --- a/packages/core/kompass.jsonc +++ b/packages/core/kompass.jsonc @@ -54,8 +54,9 @@ "ticket_load": { "enabled": true }, "ticket_sync": { "enabled": true }, "worktree_list": { "enabled": true }, + "worktree_create": { "enabled": true }, "session_create": { "enabled": true }, - "session_list": { "enabled": true }, + "session_list": { "enabled": true }, // OpenCode 1 only "session_read": { "enabled": true }, "session_send": { "enabled": true }, "session_wait": { "enabled": true }, @@ -88,7 +89,7 @@ "agentMode": "all", "navigator": { "enabled": true, - "maxConcurrentSessions": 8, + "maxConcurrentSessions": 8, // OpenCode 1 only; V2 plugins lack active-session discovery "maxReadChars": 20000, "maxOutputCharsPerItem": 4000, "maxWaitMs": 120000, diff --git a/packages/core/lib/config.ts b/packages/core/lib/config.ts index e2ee245..1e92a3e 100644 --- a/packages/core/lib/config.ts +++ b/packages/core/lib/config.ts @@ -21,6 +21,7 @@ export const DEFAULT_TOOL_NAMES = [ "ticket_sync", "ticket_load", "worktree_list", + "worktree_create", "session_create", "session_list", "session_read", @@ -32,6 +33,7 @@ export const DEFAULT_TOOL_NAMES = [ export const NAVIGATOR_TOOL_NAMES = [ "worktree_list", + "worktree_create", "session_create", "session_list", "session_read", @@ -41,6 +43,18 @@ export const NAVIGATOR_TOOL_NAMES = [ "worktree_remove", ] as const; +// V2 lacks session listing. Worktree removal uses native force semantics. +export const V2_NAVIGATOR_TOOL_NAMES = [ + "worktree_list", + "worktree_create", + "worktree_remove", + "session_create", + "session_read", + "session_send", + "session_wait", + "session_interrupt", +] as const satisfies readonly (typeof NAVIGATOR_TOOL_NAMES)[number][]; + export const DEFAULT_COMMAND_NAMES = [ "ask", "branch", @@ -165,6 +179,7 @@ export interface KompassConfig { ticket_sync?: ToolConfig; ticket_load?: ToolConfig; worktree_list?: ToolConfig; + worktree_create?: ToolConfig; session_create?: ToolConfig; session_list?: ToolConfig; session_read?: ToolConfig; @@ -227,6 +242,7 @@ export interface MergedKompassConfig { ticket_sync: ToolConfig; ticket_load: ToolConfig; worktree_list: ToolConfig; + worktree_create: ToolConfig; session_create: ToolConfig; session_list: ToolConfig; session_read: ToolConfig; @@ -252,6 +268,7 @@ export interface MergedKompassConfig { export interface NavigatorConfig { enabled: boolean; + /** OpenCode 1 only; V2 plugins cannot discover all active sessions. */ maxConcurrentSessions: number; maxReadChars: number; maxOutputCharsPerItem: number; @@ -527,6 +544,7 @@ const defaultToolConfig: Record = { ticket_sync: { enabled: true }, ticket_load: { enabled: true }, worktree_list: { enabled: true }, + worktree_create: { enabled: true }, session_create: { enabled: true }, session_list: { enabled: true }, session_read: { enabled: true }, @@ -710,6 +728,7 @@ export function mergeWithDefaults( ticket_sync: { ...defaultToolConfig.ticket_sync, ...config?.tools?.ticket_sync }, ticket_load: { ...defaultToolConfig.ticket_load, ...config?.tools?.ticket_load }, worktree_list: { ...defaultToolConfig.worktree_list, ...config?.tools?.worktree_list }, + worktree_create: { ...defaultToolConfig.worktree_create, ...config?.tools?.worktree_create }, session_create: { ...defaultToolConfig.session_create, ...config?.tools?.session_create }, session_list: { ...defaultToolConfig.session_list, ...config?.tools?.session_list }, session_read: { ...defaultToolConfig.session_read, ...config?.tools?.session_read }, diff --git a/packages/opencode/.opencode/kompass.jsonc b/packages/opencode/.opencode/kompass.jsonc index 7eface7..69e27d8 100644 --- a/packages/opencode/.opencode/kompass.jsonc +++ b/packages/opencode/.opencode/kompass.jsonc @@ -122,6 +122,9 @@ "worktree_list": { "enabled": true }, + "worktree_create": { + "enabled": true + }, "session_create": { "enabled": true }, diff --git a/packages/opencode/README.md b/packages/opencode/README.md index ad3259d..2aee792 100644 --- a/packages/opencode/README.md +++ b/packages/opencode/README.md @@ -20,7 +20,7 @@ Kompass keeps AI coding agents on course with token-efficient, composable workfl - Commands cover direct work (`/ask`, `/commit`, `/merge`, `/skill/create`, `/skill/optimize`), orchestration (`/dev`, `/ship`, `/todo`, `/pr/fix/loop`), ticket planning/sync, and PR review/shipping flows. `/branch/inline`, `/commit/inline`, `/commit-and-push/inline`, `/pr/create/inline`, and `/ship/inline` reuse the invoking session instead of starting a subtask. - Agents are intentionally narrow: `worker` handles implementation and multi-step workflows, `planner` is no-edit planning, and `reviewer` is a no-edit review specialist. -- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. OpenCode 1 also includes the eight Navigator session and worktree tools. +- Structured tools keep workflows grounded in repo and GitHub state: `changes_load`, `pr_load`, `pr_load_review`, `pr_sync`, `ticket_load`, `ticket_sync`. Navigator adds nine session/worktree tools on OpenCode 1 and eight native orchestration tools on OpenCode 2. - Reusable command-template components live in `packages/core/components/` and are documented in the components reference. ## Prerequisites @@ -64,7 +64,7 @@ The recommended project override path is `.opencode/kompass.jsonc`. ## OpenCode 2 worktrees -Kompass registers `rift` as a native OpenCode 2 worktree strategy using its bundled `rift-snapshot` dependency. It becomes the location's default strategy and works with OpenCode's worktree actions and `ctx.worktree.create`, `list`, `refresh`, and `remove`. This requires a recent V2 release with plugin worktree strategies and returned-directory support; the adapter targets `@opencode/plugin` beta `19381`. +Kompass registers `rift` as a native OpenCode 2 worktree strategy using its bundled `rift-snapshot` dependency. It becomes the location's default for API calls that omit a strategy. Desktop's current New worktree action explicitly selects Git, so that button does not use Rift; Kompass's `worktree_create` tool uses the registered default. This requires a recent V2 release with plugin worktree strategies and returned-directory support; the adapter targets `@opencode/plugin` beta `19381`. Load Kompass in OpenCode's `opencode.jsonc`; no destination setting is needed: @@ -80,9 +80,35 @@ Inventory includes sibling and nested snapshots. Removal requests force confirma The last active strategy registration wins. Unloading Kompass restores the previous default; existing snapshots keep their Rift owner and need the plugin available for removal. Rift errors are surfaced without a Git fallback. The strategy uses Rift's native bindings, loaded on first use, and requires a supported copy-on-write filesystem. Its synchronous native calls cannot be interrupted mid-operation; cancellation is checked between calls. -## Kompass Navigator +## OpenCode 2 session orchestration -Navigator is available through the OpenCode 1 plugin API for explicitly requested orchestration of native sessions in the current checkout and OpenCode-managed Git worktrees. Its eight tools have not been ported to the OpenCode 2 plugin API; V2 uses the native Rift worktree strategy described above. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. It is enabled by default on OpenCode 1, follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on one compatible API, returns immediately after admitting prompts, and supports parallel sessions. The V1 Navigator adapter polls the active-session API locally when using the V2 session protocol. It requires OpenCode `1.17.12` or newer. +Worktrees and sessions have separate lifecycles. `kompass_worktree_create({ name?, strategy?, branch? })` creates a worktree without a session. Pass its returned `directory` to `session_create`. Omit `directory` to use the calling session's current location, including when it is already in a worktree. Omit `prompt` to create an idle session, then start it with `session_send`. With a prompt, creation starts execution immediately. `session_send` targets a session ID and uses that session's location. + +`kompass_worktree_remove({ directory, force? })` removes a managed worktree through OpenCode. It rejects the checkout and any worktree containing the calling session or plugin. `force` defaults to false; true permits discarding uncommitted files. Rift still rejects snapshots with children. V2 cannot discover other active sessions, so finish or interrupt known sessions in that directory before removal. Removing a worktree does not delete its sessions. + +Navigator is enabled by default on V2 and exposes `kompass_worktree_create`, `kompass_worktree_list`, `kompass_worktree_remove`, `kompass_session_create`, `kompass_session_read`, `kompass_session_send`, `kompass_session_wait`, and `kompass_session_interrupt`. Use these for explicitly requested native-session workflows. Ordinary delegation uses OpenCode's built-in subagent tool. + +For example, call `kompass_worktree_create` with `{ "name": "parser-fix" }`, then pass the returned directory to `kompass_session_create`: + +```json +{ + "prompt": "Implement the parser fix and run its tests.", + "title": "Parser fix", + "directory": "/projects/.rifts/repo/parser-fix" +} +``` + +Session creation inherits the caller's agent/model/variant unless overridden, admits the optional first prompt, and returns the session ID and directory immediately. Explicit directories may select the project checkout or a managed worktree; relative paths resolve from the caller's directory. Session creation never creates or removes worktrees. V2 model overrides use `{ "providerID": "...", "id": "...", "variant": "..." }`. On `worktree_create`, a starting `branch` requires `strategy: "git"`; Rift uses the source checkout's state. + +Use the returned session ID to send steered or queued follow-ups, read bounded current context, wait for completion, or interrupt execution. Waits use the native Effect API, support one to eight targets, and release wait subscriptions on timeout or cancellation without interrupting the target sessions. `timeoutMs` must be positive and is capped by `maxWaitMs`. Reads use the current model context, not paginated full history. Partial failures report known resources and preserve them for recovery. + +The existing `adapters.opencode.navigator.enabled`, tool aliases/disables, `maxReadChars`, `maxOutputCharsPerItem`, and `maxWaitMs` settings apply to V2. `session_list` and the `maxConcurrentSessions` limit remain V1-only because the V2 plugin context lacks session discovery. Known session IDs continue to work after plugin reloads; Kompass does not keep its own session registry. + +## OpenCode 1 Navigator + +V1 also uses `session_create({ directory?, prompt?, agent?, model? })`: omit directory for the calling session's location, or pass the directory from a separate `worktree_create` call. The old `environment` argument has been removed. Omit prompt for an idle session. On the legacy session protocol, agent/model selection happens only when prompting, so supply those overrides to `session_send` when starting an idle session. V1 model overrides use `modelID`; V2 uses `id`. + +Navigator's OpenCode 1 adapter provides nine tools, including standalone `worktree_create({ name?, startCommand? })`. It prefers Rift when available; `startCommand` selects Git. V1 removal checks active sessions and has no force option. Ordinary delegation uses OpenCode's built-in `task` tool. Navigator follows Desktop's protocol detection and requires OpenCode `1.17.12` or newer. Configure Navigator and its limits with: @@ -108,7 +134,7 @@ Set `adapters.opencode.navigator.enabled` to `false` to disable all Navigator to Navigator accepts only sessions from the current OpenCode project and only worktrees returned by OpenCode. New sessions inherit the calling session's agent, model, and variant unless explicitly overridden. It rejects self-targeting lifecycle calls, arbitrary directories, unknown V2 agent overrides, main-checkout removal, unmanaged worktrees, and removal while a worktree has active sessions. `session_send` can switch the target session's agent or model before admitting a steered prompt when the detected OpenCode protocol supports it. `session_wait` defaults to `maxWaitMs`, caps requested timeouts at `maxWaitMs`, and treats `timeoutMs: 0` as an immediate snapshot. Navigator never force-removes or automatically cleans up resources after a partial failure. -On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. Navigator automatically uses that adapter for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees only when the experimental API or Rift adapter is unavailable. +On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter backed by its bundled `rift-snapshot` dependency. `worktree_create` uses that adapter when no `startCommand` is requested, using Git worktrees otherwise. Pass its returned directory to `session_create`. Kompass preserves the OpenCode workspace ID when it creates future sessions in Rift workspaces. Existing sessions without workspace identity are not migrated automatically. Rift workspaces are experimental OpenCode workspaces, not legacy Desktop sandboxes, and Kompass does not modify `project.sandboxes` or OpenCode's workspace database. diff --git a/packages/opencode/index.ts b/packages/opencode/index.ts index 8fcdf10..b997a5f 100644 --- a/packages/opencode/index.ts +++ b/packages/opencode/index.ts @@ -7,7 +7,7 @@ import { export const OpenCodeCompassPlugin = { id: "kompass", server: OpenCodeCompassPluginV1, - setup: setupOpenCodeV2, + effect: OpenCodeCompassPluginV2.effect, }; export { diff --git a/packages/opencode/kompass.jsonc b/packages/opencode/kompass.jsonc index 047f44a..fddc71c 100644 --- a/packages/opencode/kompass.jsonc +++ b/packages/opencode/kompass.jsonc @@ -54,8 +54,9 @@ "ticket_load": { "enabled": true }, "ticket_sync": { "enabled": true }, "worktree_list": { "enabled": true }, + "worktree_create": { "enabled": true }, "session_create": { "enabled": true }, - "session_list": { "enabled": true }, + "session_list": { "enabled": true }, // OpenCode 1 only "session_read": { "enabled": true }, "session_send": { "enabled": true }, "session_wait": { "enabled": true }, @@ -88,7 +89,7 @@ "agentMode": "all", "navigator": { "enabled": true, - "maxConcurrentSessions": 8, + "maxConcurrentSessions": 8, // OpenCode 1 only; V2 plugins lack active-session discovery "maxReadChars": 20000, "maxOutputCharsPerItem": 4000, "maxWaitMs": 120000, diff --git a/packages/opencode/navigator-v2.ts b/packages/opencode/navigator-v2.ts new file mode 100644 index 0000000..f781120 --- /dev/null +++ b/packages/opencode/navigator-v2.ts @@ -0,0 +1,252 @@ +import { Agent, Location, Model, Worktree, type Plugin } from "@opencode/plugin/effect"; +import { Session } from "@opencode/schema/session"; +import { SessionMessage } from "@opencode/schema/session-message"; +import { Tool } from "@opencode/schema/tool"; +import { DateTime, Effect, Option, Schema } from "effect"; +import path from "node:path"; +import { z } from "zod"; +import { V2_NAVIGATOR_TOOL_NAMES, type MergedKompassConfig, type NavigatorConfig } from "../core/index.ts"; +import { getConfiguredOpenCodeToolName } from "./tool-names.ts"; + +const guidance = "Use only when the user explicitly asks to create or manage native OpenCode sessions, worktrees, or a multi-session workflow. Use OpenCode's subagent tool for ordinary delegation."; +const sessionIDSchema = z.string().startsWith("ses_"); +const modelSchema = z.object({ + providerID: z.string().min(1), + id: z.string().min(1), + variant: z.string().min(1).optional(), +}); +const deliverySchema = z.enum(["steer", "queue"]).default("steer"); +const worktreeCreateSchema = z.object({ + name: z.string().min(1).optional(), + strategy: z.enum(["rift", "git"]).optional(), + branch: z.string().min(1).optional(), +}); + +function message(error: unknown) { + return error instanceof Error ? error.message : String(error); +} + +function summary(session: Session.Info) { + return { + sessionID: session.id, + projectID: session.projectID, + directory: session.location.directory, + workspaceID: session.location.workspaceID, + title: session.title, + agent: session.agent, + model: session.model, + outcome: session.outcome, + createdAt: DateTime.toEpochMillis(session.time.created), + updatedAt: DateTime.toEpochMillis(session.time.updated), + }; +} + +export function summarizeV2Messages( + messages: readonly SessionMessage.Info[], + config: NavigatorConfig, + options: { turnLimit?: number; includeOutputs?: boolean; maxOutputCharsPerItem?: number } = {}, +) { + const perItem = Math.min(options.maxOutputCharsPerItem ?? config.maxOutputCharsPerItem, config.maxOutputCharsPerItem, config.maxReadChars); + let truncated = false; + const text = (value: string) => { + if (value.length > perItem) truncated = true; + return value.slice(0, perItem); + }; + const selected = messages.slice(-(options.turnLimit ?? 20)).map((entry) => ({ + id: entry.id, + type: entry.type, + createdAt: DateTime.toEpochMillis(entry.time.created), + ...("text" in entry && typeof entry.text === "string" ? { text: text(entry.text) } : {}), + ...(entry.type === "assistant" ? { + error: entry.error?.message, + items: entry.content.map((item) => { + if (item.type !== "tool") return { type: item.type, text: text(item.text) }; + return { + type: "tool", + name: item.name, + status: item.state.status, + ...(item.state.status === "error" ? { error: text(item.state.error.message) } : {}), + ...(options.includeOutputs && "content" in item.state ? { + output: text((item.state.content ?? []).flatMap((part) => part.type === "text" ? [part.text] : []).join("\n")), + } : {}), + }; + }), + } : {}), + })); + while (selected.length && JSON.stringify(selected).length > config.maxReadChars) { + selected.shift(); + truncated = true; + } + return { messages: selected, truncation: { truncated: truncated || selected.length < messages.length, totalMessages: messages.length, returnedMessages: selected.length } }; +} + +export function createNavigatorV2Tools(ctx: Plugin.Context, config: MergedKompassConfig): Tool.Info[] { + if (!config.adapters.opencode.navigator.enabled) return []; + const limits = config.adapters.opencode.navigator; + const createWorktree = Effect.fn("navigator.createWorktree")(function* (location: Location.Ref, input: z.infer) { + if (location.workspaceID) return yield* Effect.fail(new Error("Worktree creation requires a local session")); + if (input.branch && input.strategy !== "git") { + return yield* Effect.fail(new Error("A starting branch/ref requires strategy: git; Rift snapshots the source checkout")); + } + return yield* ctx.worktree.create({ location, name: input.name, strategy: input.strategy ? Worktree.StrategyID.make(input.strategy) : undefined, branch: input.branch }); + }); + const owned = Effect.fn("navigator.owned")(function* (sessionID: string) { + const session = yield* ctx.session.get({ sessionID: Session.ID.make(sessionID) }); + if (session.projectID !== ctx.location.project.id) { + return yield* Effect.fail(new Error(`Session ${sessionID} belongs to another OpenCode project`)); + } + return session; + }); + const target = Effect.fn("navigator.target")(function* (sessionID: string, callerID: string) { + if (sessionID === callerID) return yield* Effect.fail(new Error("Navigator cannot send to, wait for, or interrupt its calling session")); + return yield* owned(sessionID); + }); + const validateSelection = Effect.fn("navigator.validateSelection")(function* (location: Location.Ref, agent?: string, model?: z.infer) { + if (agent) { + yield* ctx.agent.get({ agentID: Agent.ID.make(agent), location }); + } + if (model) { + const models = yield* ctx.catalog.model.list({ location }); + const selected = models.data.find((item) => item.providerID === model.providerID && item.id === model.id && item.enabled); + if (!selected) return yield* Effect.fail(new Error(`Unknown or disabled OpenCode model ${model.providerID}/${model.id}`)); + if (model.variant && !selected.variants.some((variant) => variant.id === model.variant)) { + return yield* Effect.fail(new Error(`Unknown model variant ${model.variant}`)); + } + } + }); + const read = Effect.fn("navigator.read")(function* (sessionID: string, options: Parameters[2] = {}) { + const session = yield* owned(sessionID); + const messages = yield* ctx.session.context({ sessionID: session.id }); + return { session: summary(session), ...summarizeV2Messages(messages, limits, options) }; + }); + function tool( + name: (typeof V2_NAVIGATOR_TOOL_NAMES)[number], + description: string, + input: S, + execute: (args: z.output, invocation: Tool.Context) => Effect.Effect, + ): Tool.Info { + return { + name: getConfiguredOpenCodeToolName(name, config.tools[name].name), + description: `${guidance} ${description}`, + input, + options: { codemode: false }, + execute: (args, invocation) => execute(args as z.output, invocation).pipe( + Effect.map((result) => ({ content: JSON.stringify(result) })), + Effect.mapError((error) => new Tool.Error({ message: message(error) })), + ), + }; + } + const definitions = { + worktree_create: tool("worktree_create", "Create a managed worktree without creating a session. Uses the caller's location as source and the registered default strategy, normally Rift. Pass the returned directory to session_create to start work there. A starting branch requires strategy: git.", worktreeCreateSchema, + Effect.fn("navigator.worktree_create")(function* (args, invocation) { + const caller = yield* owned(invocation.sessionID); + const worktree = yield* createWorktree(caller.location, args); + return { status: "created", worktree }; + })), + worktree_remove: tool("worktree_remove", "Remove a managed worktree through OpenCode. Does not delete sessions or interrupt them. V2 cannot discover active sessions: finish or interrupt known sessions using this directory first. force defaults to false and permits discarding uncommitted files when true. Rift snapshots with children cannot be removed, even with force.", z.object({ directory: z.string().min(1), force: z.boolean().default(false) }), + Effect.fn("navigator.worktree_remove")(function* (args, invocation) { + const caller = yield* owned(invocation.sessionID); + const directory = path.resolve(args.directory); + const within = (root: string) => { + const relative = path.relative(directory, root); + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); + }; + if (within(ctx.location.project.canonical) || within(caller.location.directory) || within(ctx.location.directory)) { + return yield* Effect.fail(new Error("Cannot remove the checkout or a worktree containing the calling session or plugin")); + } + const inventory = yield* ctx.worktree.list({ location: caller.location }); + const selected = inventory.find((entry) => entry.strategy !== undefined && path.resolve(entry.directory) === directory); + if (!selected) return yield* Effect.fail(new Error("The requested directory is not a managed OpenCode worktree for this project")); + yield* ctx.worktree.remove({ location: caller.location, directory: selected.directory, force: args.force }); + return { status: "removed", directory: selected.directory }; + })), + worktree_list: tool("worktree_list", "List this project's checkout and native managed worktrees, including Rift snapshots.", z.object({}), + Effect.fn("navigator.worktree_list")(function* (_args, invocation) { + const caller = yield* owned(invocation.sessionID); + const inventory = yield* ctx.worktree.list({ location: caller.location }); + return { checkout: { directory: ctx.location.project.canonical }, worktrees: inventory.filter((entry) => entry.strategy !== undefined) }; + })), + session_create: tool("session_create", "Create a native session. directory defaults to the calling session's current directory; supply a checkout or managed worktree directory to work elsewhere. To use a new worktree, call worktree_create first and pass its returned directory. Optional prompt starts execution immediately; omit it to create an idle session and use session_send later.", z.object({ + prompt: z.string().min(1).optional(), + title: z.string().min(1).optional(), + directory: z.string().min(1).optional(), + agent: z.string().min(1).optional(), + model: modelSchema.optional(), + }).strict(), Effect.fn("navigator.session_create")(function* (args, invocation) { + const caller = yield* owned(invocation.sessionID); + const agent = args.agent ?? caller.agent; + const model = args.model ?? caller.model; + let location = caller.location; + const directory = args.directory ? path.resolve(caller.location.directory, args.directory) : caller.location.directory; + if (directory !== caller.location.directory) { + if (caller.location.workspaceID) return yield* Effect.fail(new Error("Selecting another directory requires a local session")); + if (directory === ctx.location.project.canonical) { + location = Location.Ref.make({ directory: ctx.location.project.canonical }); + } else { + const inventory = yield* ctx.worktree.list({ location: caller.location }); + const selected = inventory.find((entry) => entry.strategy !== undefined && path.resolve(entry.directory) === directory); + if (!selected) return yield* Effect.fail(new Error("The requested directory is not a managed OpenCode worktree for this project")); + location = Location.Ref.make({ directory: selected.directory }); + } + } + yield* validateSelection(location, agent, model); + let session: Session.Info | undefined; + let requestedSessionID: Session.ID | undefined; + let phase = "session_create"; + return yield* Effect.gen(function* () { + requestedSessionID = Session.ID.create(); + session = yield* ctx.session.create({ id: requestedSessionID, location, title: args.title, agent: agent ? Agent.ID.make(agent) : undefined, model: model ? Schema.decodeUnknownSync(Model.Ref)(model) : undefined }); + yield* invocation.progress({ sessionID: session.id, directory: session.location.directory, phase: "session_created" }); + if (session.projectID !== ctx.location.project.id || session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID) { + return yield* Effect.fail(new Error("OpenCode created the session in an unexpected project or location")); + } + phase = "prompt"; + if (!args.prompt) return { status: "created", admitted: false, session: summary(session) }; + const admitted = yield* ctx.session.prompt({ sessionID: session.id, text: args.prompt, delivery: "steer" }); + return { status: "started", admitted: true, messageID: admitted.id, session: summary(session) }; + }).pipe(Effect.catch((error) => Effect.succeed({ + status: "partial_failure", admitted: false, phase, error: message(error), + requestedSessionID, session: session ? summary(session) : undefined, + directory: location.directory, + recovery: "The session is preserved. Read the returned session ID before retrying a prompt.", + }))); + })), + session_read: tool("session_read", "Read a bounded snapshot of a current-project session's current model context, including recent messages. This is not paginated full history.", z.object({ + sessionID: sessionIDSchema, + turnLimit: z.number().int().positive().max(100).optional(), + includeOutputs: z.boolean().optional(), + maxOutputCharsPerItem: z.number().int().positive().optional(), + }), (args) => read(args.sessionID, args)), + session_send: tool("session_send", "Admit another prompt to a current-project session, with steer or queue delivery and optional agent/model changes.", z.object({ + sessionID: sessionIDSchema, prompt: z.string().min(1), delivery: deliverySchema, + agent: z.string().min(1).optional(), model: modelSchema.optional(), + }), Effect.fn("navigator.session_send")(function* (args, invocation) { + const session = yield* target(args.sessionID, invocation.sessionID); + yield* validateSelection(session.location, args.agent, args.model); + if (args.agent) yield* ctx.session.switchAgent({ sessionID: session.id, agent: Agent.ID.make(args.agent) }); + if (args.model) yield* ctx.session.switchModel({ sessionID: session.id, model: Schema.decodeUnknownSync(Model.Ref)(args.model) }); + const admitted = yield* ctx.session.prompt({ sessionID: session.id, text: args.prompt, delivery: args.delivery }); + return { sessionID: session.id, admitted: true, messageID: admitted.id, delivery: args.delivery }; + })), + session_wait: tool("session_wait", "Wait for the first target session to become idle using OpenCode's native wait. Timeout or cancellation ends only the wait, not the target sessions. Completion includes the session outcome and bounded context.", z.object({ + targets: z.array(z.object({ sessionID: sessionIDSchema })).min(1).max(8), + timeoutMs: z.number().int().positive().optional(), + }), Effect.fn("navigator.session_wait")(function* (args, invocation) { + const ids = args.targets.map((item) => item.sessionID); + if (new Set(ids).size !== ids.length) return yield* Effect.fail(new Error("Navigator wait targets must be unique")); + const sessions = yield* Effect.all(ids.map((id) => target(id, invocation.sessionID)), { concurrency: "unbounded" }); + const completed = yield* Effect.raceAllFirst(sessions.map((session) => ctx.session.wait({ sessionID: session.id }).pipe(Effect.as(session.id)))).pipe( + Effect.timeoutOption(Math.min(args.timeoutMs ?? limits.maxWaitMs, limits.maxWaitMs)), + ); + if (Option.isNone(completed)) return { status: "timeout", targets: args.targets }; + return { status: "completed", completed: yield* read(completed.value) }; + })), + session_interrupt: tool("session_interrupt", "Interrupt a current-project session through OpenCode's native lifecycle API.", z.object({ sessionID: sessionIDSchema }), + Effect.fn("navigator.session_interrupt")(function* (args, invocation) { + const session = yield* target(args.sessionID, invocation.sessionID); + const result = yield* ctx.session.interrupt({ sessionID: session.id, continue: false }); + return { sessionID: session.id, ...result }; + })), + }; + return V2_NAVIGATOR_TOOL_NAMES.filter((name) => config.tools[name].enabled !== false).map((name) => definitions[name]); +} diff --git a/packages/opencode/navigator.ts b/packages/opencode/navigator.ts index e03a655..d036f97 100644 --- a/packages/opencode/navigator.ts +++ b/packages/opencode/navigator.ts @@ -73,6 +73,7 @@ export interface MessageSummary { type NavigatorToolName = | "worktree_list" + | "worktree_create" | "session_create" | "session_list" | "session_read" @@ -236,33 +237,6 @@ async function listManagedWorktrees(client: NavigatorClient, checkout: string, p return [...unique.values()]; } -async function removeCreatedWorkspace(client: NavigatorClient, checkout: string, workspace: NativeWorktree) { - if (workspace.type === "rift") { - const api = workspaceApi(client); - if (!api?.workspace?.remove || !workspace.id) throw new Error("OpenCode Rift workspace removal is unavailable"); - const response = await api.workspace.remove({ id: workspace.id, directory: checkout }); - if (response.error !== undefined) failResponse(response.error, `OpenCode Rift workspace rollback for ${workspace.directory}`); - return; - } - responseData(await client.worktree.remove({ - directory: checkout, - worktreeRemoveInput: { directory: workspace.directory }, - }), `OpenCode worktree rollback for ${workspace.directory}`); -} - -async function rollbackCreatedWorkspaces(client: NavigatorClient, checkout: string, workspaces: NativeWorktree[]) { - const unique = new Map(workspaces.map((item) => [normalizeDirectory(item.directory), item])); - const failures: string[] = []; - for (const workspace of unique.values()) { - try { - await removeCreatedWorkspace(client, checkout, workspace); - } catch (error) { - failures.push(`${workspace.directory}: ${error instanceof Error ? error.message : String(error)}`); - } - } - return failures; -} - async function activeSessionIDs(client: NavigatorClient, navigator: NavigatorContext) { if (navigator.protocol === "v1") { const worktrees = await listManagedWorktrees(client, navigator.checkout, navigator.projectID); @@ -562,11 +536,44 @@ export function createNavigatorTools( input: NavigatorContext & { client: NavigatorClient }, ): Record { const { client, ...navigator } = input; + async function createManagedWorktree(args: { name?: string; startCommand?: string }): Promise { + const api = workspaceApi(client); + if (!args.startCommand && await hasRiftAdapter(client, navigator.checkout)) { + if (!api?.workspace?.create) throw new Error("Rift workspace creation is unavailable"); + const workspace = responseData(await api.workspace.create({ + directory: navigator.checkout, type: "rift", extra: args.name ? { name: args.name } : undefined, + }), "OpenCode Rift workspace create"); + const worktree = normalizeRiftWorkspace(workspace); + if (!worktree) throw new Error("OpenCode Rift workspace create returned no directory"); + return worktree; + } + return normalizeWorktree(responseData(await client.worktree.create({ + directory: navigator.checkout, + worktreeCreateInput: { + ...(args.name ? { name: args.name } : {}), + ...(args.startCommand ? { startCommand: args.startCommand } : {}), + }, + }), "OpenCode worktree create")); + } const agentNames = [...new Set(navigator.agentNames ?? [])].sort(); const agentSchema = agentNames.length ? tool.schema.enum(agentNames as [string, ...string[]]) : tool.schema.string().min(1); return { + worktree_create: tool({ + description: `${explicitNavigatorUse} Create a managed worktree without a session. Pass its returned directory to session_create. Prefers Rift when available; startCommand selects Git.`, + args: { + name: tool.schema.string().min(1).optional(), + startCommand: tool.schema.string().min(1).optional(), + }, + async execute(args) { + const worktree = await createManagedWorktree(args); + if (worktree.projectID && worktree.projectID !== navigator.projectID) { + throw new Error(`Created workspace ${worktree.directory} belongs to another project; it was preserved for recovery`); + } + return json({ status: "created", worktree }); + }, + }), worktree_list: tool({ description: `${explicitNavigatorUse} List the current OpenCode project checkout and its managed native worktrees.`, args: {}, @@ -579,21 +586,10 @@ export function createNavigatorTools( }), session_create: tool({ - description: `${explicitNavigatorUse} Create and asynchronously prompt a native OpenCode session in the checkout or a managed worktree.`, + description: `${explicitNavigatorUse} Create a native session. directory defaults to the calling session's current directory. To use a new worktree, call worktree_create first and pass its returned directory. Optional prompt starts execution immediately; omit it to create an idle session and use session_send later.`, args: { - prompt: tool.schema.string().min(1), - environment: tool.schema.discriminatedUnion("type", [ - tool.schema.object({ type: tool.schema.literal("checkout") }), - tool.schema.object({ - type: tool.schema.literal("existing_worktree"), - directory: tool.schema.string().min(1), - }), - tool.schema.object({ - type: tool.schema.literal("new_worktree"), - name: tool.schema.string().min(1).optional(), - startCommand: tool.schema.string().min(1).optional(), - }), - ]), + prompt: tool.schema.string().min(1).optional(), + directory: tool.schema.string().min(1).optional(), agent: agentSchema.describe("Registered OpenCode agent; omit to inherit the caller/default agent").optional(), model: tool.schema.object({ providerID: tool.schema.string().min(1), @@ -602,14 +598,18 @@ export function createNavigatorTools( }).optional(), }, async execute(args, context) { - const active = await activeSessionIDs(client, navigator); + if ("environment" in args) throw new Error("environment is no longer supported; call worktree_create separately and pass directory"); + if (navigator.protocol === "v1" && !args.prompt && (args.agent || args.model)) { + throw new Error("The legacy session API selects agent/model when prompting. Supply prompt or pass those overrides to session_send later."); + } + const active = args.prompt ? await activeSessionIDs(client, navigator) : new Set(); const activeSessions = await Promise.all([...active].map((sessionID) => getSession(client, sessionID))); const activeOwnedCount = activeSessions.filter((session) => session.projectID === navigator.projectID).length; if (activeOwnedCount >= navigator.config.maxConcurrentSessions) { throw new Error(`Navigator allows at most ${navigator.config.maxConcurrentSessions} concurrent sessions`); } - const caller = await getOwnedSession(client, navigator.projectID, context.sessionID).catch(() => undefined); + const caller = await getOwnedSession(client, navigator.projectID, context.sessionID); const selectedAgent = args.agent ?? caller?.agent; const selectedModel = args.model ?? (caller?.model ? { @@ -619,10 +619,11 @@ export function createNavigatorTools( } : undefined); - let location: NavigatorLocation = { directory: navigator.checkout }; - let createdWorktree: NativeWorktree | undefined; - if (args.environment.type === "existing_worktree") { - const requestedDirectory = args.environment.directory; + let location: NavigatorLocation = caller.location; + const requestedDirectory = args.directory ? path.resolve(caller.location.directory, args.directory) : caller.location.directory; + if (!sameDirectory(requestedDirectory, caller.location.directory) && sameDirectory(requestedDirectory, navigator.checkout)) { + location = { directory: navigator.checkout }; + } else if (!sameDirectory(requestedDirectory, caller.location.directory)) { const worktrees = await listManagedWorktrees(client, navigator.checkout, navigator.projectID); const requested = worktrees.find((item) => sameDirectory(item.directory, requestedDirectory)); if (!requested) throw new Error("The requested directory is not a managed OpenCode workspace for this project"); @@ -630,60 +631,6 @@ export function createNavigatorTools( directory: requested.directory, ...(requested.workspaceID ? { workspaceID: requested.workspaceID } : {}), }; - } else if (args.environment.type === "new_worktree") { - const worktreesBefore = await listManagedWorktrees(client, navigator.checkout, navigator.projectID); - try { - const api = workspaceApi(client); - const useRift = !args.environment.startCommand && await hasRiftAdapter(client, navigator.checkout); - if (useRift) { - if (!api?.workspace?.create) { - throw new Error("OpenCode advertises the Rift workspace adapter but workspace creation is unavailable"); - } - const workspace = responseData(await api.workspace.create({ - directory: navigator.checkout, - type: "rift", - extra: args.environment.name ? { name: args.environment.name } : undefined, - }), "OpenCode Rift workspace create"); - const normalized = normalizeRiftWorkspace(workspace); - if (!normalized) throw new Error("OpenCode Rift workspace create returned no directory"); - createdWorktree = normalized; - if (normalized.projectID && normalized.projectID !== navigator.projectID) { - throw new Error( - `OpenCode registered workspace ${normalized.id} to project ${normalized.projectID}; expected ${navigator.projectID}`, - ); - } - } else { - const worktree = responseData( - await client.worktree.create({ - directory: navigator.checkout, - worktreeCreateInput: { - ...(args.environment.name ? { name: args.environment.name } : {}), - ...(args.environment.startCommand ? { startCommand: args.environment.startCommand } : {}), - }, - }), - "OpenCode worktree create", - ); - createdWorktree = normalizeWorktree(worktree); - } - location = { - directory: createdWorktree.directory, - ...(createdWorktree.workspaceID ? { workspaceID: createdWorktree.workspaceID } : {}), - }; - } catch (error) { - const worktreesAfter = await listManagedWorktrees(client, navigator.checkout, navigator.projectID).catch(() => []); - const before = new Set(worktreesBefore.map((item) => normalizeDirectory(item.directory))); - const partial = worktreesAfter.filter((item) => !before.has(normalizeDirectory(item.directory))); - const created = createdWorktree ? [createdWorktree, ...partial] : partial; - const rollbackFailures = await rollbackCreatedWorkspaces(client, navigator.checkout, created); - throw new Error( - `Navigator could not create the requested workspace: ${error instanceof Error ? error.message : String(error)}` + - (created.length - ? rollbackFailures.length - ? `. Rollback failed for ${rollbackFailures.join("; ")}` - : ". The partially created workspace was removed" - : ""), - ); - } } let session: SessionV2Info | { id: string; projectID: string }; @@ -716,16 +663,8 @@ export function createNavigatorTools( "OpenCode session create", ); } catch (error) { - const rollbackFailures = createdWorktree - ? await rollbackCreatedWorkspaces(client, navigator.checkout, [createdWorktree]) - : []; throw new Error( - `Navigator session creation failed: ${error instanceof Error ? error.message : String(error)}` + - (createdWorktree - ? rollbackFailures.length - ? `. Workspace created at ${createdWorktree.directory}, but rollback failed: ${rollbackFailures.join("; ")}` - : `. Workspace ${createdWorktree.directory} was rolled back` - : ""), + `Navigator session creation failed: ${error instanceof Error ? error.message : String(error)}`, ); } const createdLocation = navigator.protocol === "v2" @@ -743,20 +682,12 @@ export function createNavigatorTools( ? `belongs to project ${session.projectID}; expected ${navigator.projectID}` : undefined; if (validationError) { - const rollbackFailures = createdWorktree - ? await rollbackCreatedWorkspaces(client, navigator.checkout, [createdWorktree]) - : []; throw new Error( - `Created session ${session.id} ${validationError}. It was not prompted` + - (createdWorktree - ? rollbackFailures.length - ? `. Workspace rollback failed: ${rollbackFailures.join("; ")}` - : `. Workspace ${createdWorktree.directory} was rolled back` - : ""), + `Created session ${session.id} ${validationError}. It was not prompted`, ); } - try { + if (args.prompt) try { if (navigator.protocol === "v1") { const response = await navigator.legacyClient(location.directory).session.promptAsync({ path: { id: session.id }, @@ -784,18 +715,16 @@ export function createNavigatorTools( } } catch (error) { throw new Error( - `Navigator prompt admission failed: ${error instanceof Error ? error.message : String(error)}. Session created: ${session.id}` + - (createdWorktree ? `. Worktree created: ${createdWorktree.name} (${createdWorktree.directory})` : ""), + `Navigator prompt admission failed: ${error instanceof Error ? error.message : String(error)}. Session created: ${session.id}`, ); } return json({ + status: args.prompt ? "started" : "created", + admitted: Boolean(args.prompt), sessionID: session.id, directory: location.directory, ...(location.workspaceID ? { workspaceID: location.workspaceID } : {}), - ...(createdWorktree - ? { worktree: { created: true, type: createdWorktree.type, name: createdWorktree.name, ...(createdWorktree.id ? { id: createdWorktree.id } : {}), ...(createdWorktree.workspaceID ? { workspaceID: createdWorktree.workspaceID } : {}), ...(createdWorktree.branch ? { branch: createdWorktree.branch } : {}) } } - : {}), }); }, }), diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 609ccb5..d91445a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -48,7 +48,9 @@ }, "dependencies": { "@opencode/plugin": "0.0.0-beta-19381", + "@opencode/schema": "0.0.0-beta-19381", "@opencode-ai/sdk": "^1.18.7", + "effect": "4.0.0-rc.112", "rift-snapshot": "^0.0.10", "zod": "^4.1.8" }, diff --git a/packages/opencode/rift-worktree.ts b/packages/opencode/rift-worktree.ts index c22bbce..28a4981 100644 --- a/packages/opencode/rift-worktree.ts +++ b/packages/opencode/rift-worktree.ts @@ -1,5 +1,6 @@ -import { Worktree } from "@opencode/plugin"; -import type { WorktreeDefinition } from "@opencode/plugin/promise/worktree"; +import { Worktree } from "@opencode/plugin/effect"; +import type { WorktreeDefinition } from "@opencode/plugin/effect/worktree"; +import { Effect, Predicate } from "effect"; import { execFile } from "node:child_process"; import { realpath } from "node:fs/promises"; import path from "node:path"; @@ -7,99 +8,91 @@ import { promisify } from "node:util"; import { createRiftSnapshot, loadRift, type RiftBackend } from "./rift.ts"; const execFileAsync = promisify(execFile); +const resolveDirectory = (directory: string) => Effect.tryPromise({ try: () => realpath(directory), catch: (error) => error }); -function isUninitialized(error: unknown) { - return error instanceof Error && "code" in error && error.code === "workspace_not_initialized"; -} - -export function createRiftWorktreeStrategy(load: () => Promise = loadRift): WorktreeDefinition { - async function backend(signal: AbortSignal) { - signal.throwIfAborted(); - const rift = await load(); - signal.throwIfAborted(); - return rift; - } - +export function createRiftWorktreeStrategy( + backend: Effect.Effect = Effect.tryPromise({ try: loadRift, catch: (error) => error }), +): WorktreeDefinition { return { id: "rift", - async create(input, { signal }) { + create: Effect.fn("rift.create")(function* (input) { if (input.branch !== undefined) { - throw new Worktree.OperationError({ + return yield* Effect.fail(new Worktree.OperationError({ message: "Rift snapshots the source checkout and cannot select a starting branch or ref. Use the git strategy for a starting ref.", - }); + })); } - const rift = await backend(signal); - // Rift's native calls are synchronous. Check cancellation before entering them; - // once creation completes, return its path so OpenCode can record ownership. + const rift = yield* backend; const name = path.basename(input.directory); - for (let attempt = 1; ; attempt++) { - signal.throwIfAborted(); - try { - const directory = createRiftSnapshot(rift, { - from: input.sourceDirectory, - name: attempt === 1 ? name : `${name}-${attempt}`, - }); - return { directory: path.resolve(directory) }; - } catch (error) { - // OpenCode checks its suggested destination, but Rift owns a different - // storage directory. Resolve collisions reported by Rift there. - if (!(error instanceof Error) || !("code" in error)) throw error; - const targetsSource = error.code === "inside_source" && "path" in error - && typeof error.path === "string" && path.resolve(error.path) === path.resolve(input.sourceDirectory); - if ((error.code !== "already_exists" && !targetsSource) || attempt >= 100) throw error; + // Rift's FFI operation is synchronous; capture its failures in the Effect + // error channel and return the created directory before yielding again. + return yield* Effect.try({ try: () => { + for (let attempt = 1; ; attempt++) { + try { + const directory = createRiftSnapshot(rift, { + from: input.sourceDirectory, + name: attempt === 1 ? name : `${name}-${attempt}`, + }); + return { directory: path.resolve(directory) }; + } catch (error) { + // OpenCode checks its suggested destination; Rift owns a different + // storage directory, so resolve collisions reported by Rift there. + if (!(error instanceof Error) || !Predicate.hasProperty(error, "code")) throw error; + const targetsSource = error.code === "inside_source" && Predicate.hasProperty(error, "path") + && Predicate.isString(error.path) && path.resolve(error.path) === path.resolve(input.sourceDirectory); + if ((error.code !== "already_exists" && !targetsSource) || attempt >= 100) throw error; + } } - } - }, - async list(sourceDirectory, { signal }) { - const rift = await backend(signal); - let ancestors: string[]; - try { - ancestors = rift.ancestors({ of: sourceDirectory }); - } catch (error) { - if (isUninitialized(error)) return []; - throw error; - } - const root = await realpath(ancestors.at(-1) ?? sourceDirectory); + }, catch: (error) => error }); + }), + list: Effect.fn("rift.list")(function* (sourceDirectory) { + const rift = yield* backend; + const ancestors = yield* Effect.try({ try: () => rift.ancestors({ of: sourceDirectory }), catch: (error) => error }).pipe( + Effect.catch((error) => error instanceof Error && Predicate.hasProperty(error, "code") && error.code === "workspace_not_initialized" + ? Effect.succeed(undefined) : Effect.fail(error)), + ); + if (!ancestors) return []; + const root = yield* resolveDirectory(ancestors.at(-1) ?? sourceDirectory); const entries: Array<{ directory: string; type: "root" | "worktree" }> = [{ directory: root, type: "root" }]; const seen = new Set([root]); - // Rift lists immediate children; OpenCode needs the whole project inventory, - // including siblings and snapshots created from another snapshot. + // Rift lists immediate children; OpenCode needs siblings and descendants. for (const entry of entries) { - signal.throwIfAborted(); - for (const child of rift.list({ of: entry.directory })) { - const directory = await realpath(child).catch((error: NodeJS.ErrnoException) => { - if (error.code === "ENOENT") return undefined; - throw error; - }); - if (!directory) continue; - if (seen.has(directory)) continue; + const children = yield* Effect.try({ try: () => rift.list({ of: entry.directory }), catch: (error) => error }); + for (const child of children) { + const directory = yield* resolveDirectory(child).pipe(Effect.catch((error) => Predicate.hasProperty(error, "code") && error.code === "ENOENT" + ? Effect.succeed(undefined) : Effect.fail(error))); + if (!directory || seen.has(directory)) continue; seen.add(directory); entries.push({ directory, type: "worktree" }); } } return entries; - }, - async remove(input, { signal }) { - const rift = await backend(signal); - const directory = await realpath(input.directory); - signal.throwIfAborted(); - const parent = rift.ancestors({ of: directory })[0]; - if (!parent || !rift.list({ of: parent }).some((child) => path.resolve(child) === directory)) { - throw new Worktree.OperationError({ message: `Not a Rift snapshot root: ${directory}` }); - } + }), + remove: Effect.fn("rift.remove")(function* (input) { + const rift = yield* backend; + const directory = yield* resolveDirectory(input.directory); + yield* Effect.try({ try: () => { + const parent = rift.ancestors({ of: directory })[0]; + if (!parent || !rift.list({ of: parent }).some((child) => path.resolve(child) === directory)) { + throw new Worktree.OperationError({ message: `Not a Rift snapshot root: ${directory}` }); + } + }, catch: (error) => error }); if (!input.force) { - const { stdout } = await execFileAsync("git", ["-C", directory, "status", "--porcelain", "--untracked-files=all"], { signal }); + const { stdout } = yield* Effect.tryPromise({ + try: (signal) => execFileAsync("git", ["-C", directory, "status", "--porcelain", "--untracked-files=all"], { signal }), + catch: (error) => error, + }); if (stdout.trim()) { - throw new Worktree.OperationError({ message: "Rift snapshot has uncommitted changes.", forceRequired: true }); + return yield* Effect.fail(new Worktree.OperationError({ message: "Rift snapshot has uncommitted changes.", forceRequired: true })); } } - signal.throwIfAborted(); - // Rift removes an entire subtree. Check after the async status call so a - // snapshot created while Git was running cannot be removed with its parent. - if (rift.list({ of: directory }).length) { - throw new Worktree.OperationError({ message: "Remove this Rift snapshot's child snapshots first." }); - } - rift.remove({ at: directory }); - }, + // Check children and remove in one synchronous operation, after Git's + // async status call, to avoid cascading into newly created snapshots. + yield* Effect.try({ try: () => { + if (rift.list({ of: directory }).length) { + throw new Worktree.OperationError({ message: "Remove this Rift snapshot's child snapshots first." }); + } + rift.remove({ at: directory }); + }, catch: (error) => error }); + }), }; } diff --git a/packages/opencode/scripts/build.ts b/packages/opencode/scripts/build.ts index ba15fc5..f0320e1 100644 --- a/packages/opencode/scripts/build.ts +++ b/packages/opencode/scripts/build.ts @@ -15,7 +15,9 @@ const packageReadme = path.join(packageRoot, "README.md"); const runtimeDirs = ["agents", "commands", "components"] as const; const bundleExternals = [ "@opencode/plugin", + "@opencode/schema", "rift-snapshot", + "effect", "@opencode-ai/sdk/v2", ] as const; const bundleArgs = [ diff --git a/packages/opencode/shared-tools.ts b/packages/opencode/shared-tools.ts index 411f2a6..9bf6ffc 100644 --- a/packages/opencode/shared-tools.ts +++ b/packages/opencode/shared-tools.ts @@ -12,6 +12,7 @@ import { type MergedKompassConfig, type Shell, type ShellPromise, + type ToolExecutionContext, } from "../core/index.ts"; export type ToolContext = { @@ -36,24 +37,25 @@ export type ToolResult = string | { metadata?: Record; }; -export function tool(input: { +export function tool(input: { description: string; args: Args; - execute(args: z.infer>, context: ToolContext): Promise; + execute(args: z.infer>, context: Context): Promise; }) { return input; } tool.schema = z; -export type ToolDefinition = ReturnType; +export type ToolDefinition = ReturnType>; +export type CoreToolDefinition = ReturnType>; const execFileAsync = promisify(execFile); type OpenCodeToolCreator = ( config: MergedKompassConfig, shell: Shell, -) => ToolDefinition; +) => CoreToolDefinition; type ShellResult = ShellPromise & { stdout: Buffer; @@ -88,10 +90,12 @@ class NodeShellCommand implements PromiseLike { #quiet = false; #nothrow = false; #result?: Promise; + #signal?: AbortSignal; - constructor(command: string, cwd: string) { + constructor(command: string, cwd: string, signal?: AbortSignal) { this.#command = command; this.#cwd = cwd; + this.#signal = signal; } cwd(dir: string) { @@ -122,16 +126,20 @@ class NodeShellCommand implements PromiseLike { } private async execute() { + this.#signal?.throwIfAborted(); try { const { stdout, stderr } = await execFileAsync("/bin/bash", ["-lc", this.#command], { cwd: this.#cwd, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, + signal: this.#signal, }); this.write(stdout, stderr); return shellResult(stdout, stderr, 0); } catch (error) { + // Cancellation must stop the workflow even for a nothrow command. + this.#signal?.throwIfAborted(); const failed = error as { stdout?: unknown; stderr?: unknown; message?: unknown; code?: unknown }; const stdout = String(failed.stdout ?? ""); const stderr = String(failed.stderr ?? failed.message ?? ""); @@ -152,7 +160,7 @@ class NodeShellCommand implements PromiseLike { } } -export function createNodeShell(defaultDirectory: string): Shell { +export function createNodeShell(defaultDirectory: string, signal?: AbortSignal): Shell { return (strings: TemplateStringsArray, ...expressions: unknown[]) => { let command = strings[0] ?? ""; @@ -163,7 +171,7 @@ export function createNodeShell(defaultDirectory: string): Shell { command += strings[index + 1] ?? ""; }); - return new NodeShellCommand(command, defaultDirectory) as unknown as ShellPromise; + return new NodeShellCommand(command, defaultDirectory, signal) as unknown as ShellPromise; }; } @@ -182,7 +190,7 @@ const opencodeToolCreators: Record = { .describe("Only load uncommitted changes (staged and unstaged), never fall back to branch comparison") .optional(), }, - execute: (args, context) => definition.execute(args, context), + execute: (args, context: ToolExecutionContext) => definition.execute(args, context), }); }, pr_load(_: MergedKompassConfig, shell: Shell) { @@ -192,7 +200,7 @@ const opencodeToolCreators: Record = { args: { pr: tool.schema.string().describe("PR number or URL").optional(), }, - execute: (args, context) => definition.execute(args, context), + execute: (args, context: ToolExecutionContext) => definition.execute(args, context), }); }, pr_load_review(_: MergedKompassConfig, shell: Shell) { @@ -203,7 +211,7 @@ const opencodeToolCreators: Record = { pr: tool.schema.string().describe("PR number or URL").optional(), since: tool.schema.string().describe("Exclusive ISO-8601 review checkpoint"), }, - execute: (args, context) => definition.execute(args, context), + execute: (args, context: ToolExecutionContext) => definition.execute(args, context), }); }, pr_sync(config: MergedKompassConfig, shell: Shell) { @@ -251,7 +259,7 @@ const opencodeToolCreators: Record = { })).describe("Replies to existing review comments").optional(), commentBody: tool.schema.string().describe("General PR comment body").optional(), }, - execute: (args, context) => definition.execute(args, context), + execute: (args, context: ToolExecutionContext) => definition.execute(args, context), }); }, ticket_sync(_: MergedKompassConfig, shell: Shell) { @@ -274,7 +282,7 @@ const opencodeToolCreators: Record = { refUrl: tool.schema.string().describe("Optional issue URL to update").optional(), comments: tool.schema.array(tool.schema.string()).describe("Optional issue comments to post").optional(), }, - execute: (args, context) => definition.execute(args, context), + execute: (args, context: ToolExecutionContext) => definition.execute(args, context), }); }, ticket_load(_: MergedKompassConfig, shell: Shell) { @@ -285,7 +293,7 @@ const opencodeToolCreators: Record = { source: tool.schema.string().describe("Issue URL, repo#id, #id, file path, or raw text"), comments: tool.schema.boolean().describe("Include issue comments").optional(), }, - execute: (args, context) => definition.execute(args, context), + execute: (args, context: ToolExecutionContext) => definition.execute(args, context), }); }, }; @@ -293,8 +301,9 @@ const opencodeToolCreators: Record = { export function createCoreOpenCodeToolDefinitions( config: MergedKompassConfig, projectRoot: string, -): Record { - const shell = createNodeShell(projectRoot); + signal?: AbortSignal, +): Record { + const shell = createNodeShell(projectRoot, signal); return Object.fromEntries( Object.entries(opencodeToolCreators).map(([name, create]) => [name, create(config, shell)]), ); diff --git a/packages/opencode/test/navigator-v2.test.ts b/packages/opencode/test/navigator-v2.test.ts new file mode 100644 index 0000000..16641c8 --- /dev/null +++ b/packages/opencode/test/navigator-v2.test.ts @@ -0,0 +1,325 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { Agent, Location, Model, type Plugin } from "@opencode/plugin/effect"; +import { Session } from "@opencode/schema/session"; +import { SessionMessage } from "@opencode/schema/session-message"; +import { SessionInbox } from "@opencode/schema/session-inbox"; +import { Tool } from "@opencode/schema/tool"; +import { Deferred, Effect, Fiber, Schema, type Types } from "effect"; +import { z } from "zod"; +import { mergeWithDefaults } from "../../core/index.ts"; +import { createNavigatorV2Tools, summarizeV2Messages } from "../navigator-v2.ts"; + +const model = Schema.decodeUnknownSync(Model.Ref)({ providerID: "test", id: "model", variant: "high" }); +function session(id = "ses_caller", directory = "/repo", projectID = "project") { + return Schema.decodeUnknownSync(Session.Info)({ + id, projectID, location: { directory }, agent: "worker", model, + title: "Task", cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 2 }, + }); +} + +function fixture() { + const calls: Array<{ name: string; input: unknown }> = []; + const sessions = new Map([ + ["ses_caller", session()], ["ses_target", session("ses_target", "/snapshots/task")], + ["ses_other", session("ses_other", "/snapshots/other")], ["ses_foreign", session("ses_foreign", "/foreign", "foreign")], + ]); + const config = mergeWithDefaults({}); + const location = Schema.decodeUnknownSync(Location.Info)({ directory: "/repo", project: { id: "project", directory: "/repo", canonical: "/repo" } }); + const methods: Types.Mutable> = { + get: ({ sessionID }) => Effect.suspend(() => { + const value = sessions.get(sessionID); + return value ? Effect.succeed(value) : Effect.fail(new Error("Missing session")); + }), + create: (input) => Effect.sync(() => { + calls.push({ name: "create", input }); + const created = { ...session(input!.id!, input!.location!.directory), agent: input?.agent, model: input?.model, title: input?.title }; + sessions.set(created.id, created); + return created; + }), + prompt: (input) => Effect.sync(() => { + calls.push({ name: "prompt", input }); + return Schema.decodeUnknownSync(SessionInbox.User)({ id: "msg_prompt", sessionID: input.sessionID, timeCreated: 3, type: "user", delivery: input.delivery, payload: { text: input.text } }); + }), + context: () => Effect.succeed([]), + wait: ({ sessionID }) => Effect.sync(() => { calls.push({ name: "wait", input: sessionID }); }), + interrupt: (input) => Effect.sync(() => { calls.push({ name: "interrupt", input }); return { interrupted: true }; }), + switchAgent: (input) => Effect.sync(() => { calls.push({ name: "switchAgent", input }); }), + switchModel: (input) => Effect.sync(() => { calls.push({ name: "switchModel", input }); }), + }; + const worktree: Types.Mutable> = { + create: (input) => Effect.sync(() => { + calls.push({ name: "worktree", input }); + return Schema.decodeUnknownSync(Location.Ref)({ directory: "/snapshots/new" }); + }), + list: () => Effect.succeed([ + { directory: location.directory }, + { directory: Schema.decodeUnknownSync(Location.Ref)({ directory: "/snapshots/task" }).directory, strategy: "rift" }, + ]), + }; + const ctx = { + location, session: methods, worktree, + agent: { get: ({ agentID }: { agentID: Agent.ID }) => agentID === "missing" + ? Effect.fail(new Error("Unknown agent")) : Effect.succeed({ location, data: Agent.Info.default(agentID) }) }, + catalog: { model: { list: () => Effect.succeed({ location, data: [{ id: model.id, providerID: model.providerID, enabled: true, variants: [{ id: "high" }] }] }) } }, + } as unknown as Plugin.Context; + const invocation = { + sessionID: Session.ID.make("ses_caller"), agent: Agent.ID.make("worker"), + messageID: SessionMessage.ID.make("msg_caller"), id: Tool.CallID.make("call"), + progress: (input) => Effect.sync(() => { calls.push({ name: "progress", input }); }), + } satisfies Tool.Context; + function execute(name: string, args: unknown) { + const tool = createNavigatorV2Tools(ctx, config).find((entry) => entry.name === `kompass_${name}`)!; + return tool.execute((tool.input as z.ZodType).parse(args), invocation).pipe(Effect.map((result) => JSON.parse(String(result.content)))); + } + return { ctx, config, calls, sessions, methods, worktree, invocation, execute }; +} + +describe("native V2 Navigator", () => { + test("creates a standalone worktree without creating or prompting a session", async () => { + const f = fixture(); + const result = await Effect.runPromise(f.execute("worktree_create", { name: "task" })); + assert.equal(result.status, "created"); + assert.equal(result.worktree.directory, "/snapshots/new"); + assert.deepEqual(f.calls.map((call) => call.name), ["worktree"]); + await assert.rejects(Effect.runPromise(f.execute("worktree_create", { branch: "main" })), /requires strategy/); + }); + + test("creates an idle session then admits a separate prompt", async () => { + const f = fixture(); + const result = await Effect.runPromise(f.execute("session_create", {})); + assert.equal(result.status, "created"); + assert.equal(result.admitted, false); + assert.equal(f.calls.some((call) => call.name === "prompt"), false); + const sent = await Effect.runPromise(f.execute("session_send", { sessionID: result.session.sessionID, prompt: "Begin" })); + assert.equal(sent.admitted, true); + }); + + test("native removal validates inventory and caller paths and forwards force", async () => { + const f = fixture(); + const calls: unknown[] = []; + Object.assign(f.worktree, { remove: (input: unknown) => Effect.sync(() => { calls.push(input); }) }); + for (const directory of ["/repo", "/", "/unknown"]) { + await assert.rejects(Effect.runPromise(f.execute("worktree_remove", { directory }))); + } + assert.deepEqual(calls, []); + const result = await Effect.runPromise(f.execute("worktree_remove", { directory: "/snapshots/task" })); + assert.equal(result.status, "removed"); + assert.deepEqual(calls[0], { location: f.sessions.get("ses_caller")!.location, directory: "/snapshots/task", force: false }); + await Effect.runPromise(f.execute("worktree_remove", { directory: "/snapshots/task", force: true })); + assert.equal((calls[1] as { force: boolean }).force, true); + f.sessions.set("ses_caller", session("ses_caller", "/snapshots/task/subdir")); + await assert.rejects(Effect.runPromise(f.execute("worktree_remove", { directory: "/snapshots/task", force: true })), /calling session/); + assert.equal(calls.length, 2); + }); + test("registers supported tools with aliases, individual disables, and the feature toggle", () => { + const f = fixture(); + assert.deepEqual(createNavigatorV2Tools(f.ctx, f.config).map((tool) => tool.name), [ + "kompass_worktree_list", "kompass_worktree_create", "kompass_worktree_remove", "kompass_session_create", "kompass_session_read", "kompass_session_send", "kompass_session_wait", "kompass_session_interrupt", + ]); + f.config.tools.session_create.name = "start_work"; + f.config.tools.session_read.enabled = false; + const names = createNavigatorV2Tools(f.ctx, f.config).map((tool) => tool.name); + assert.ok(names.includes("start_work")); + assert.ok(!names.includes("kompass_session_read")); + f.config.adapters.opencode.navigator.enabled = false; + assert.deepEqual(createNavigatorV2Tools(f.ctx, f.config), []); + }); + + test("creates a default-strategy worktree, inherits context, and admits its prompt", async () => { + const f = fixture(); + const created = await Effect.runPromise(f.execute("worktree_create", { name: "task" })); + f.worktree.list = () => Effect.succeed([{ directory: created.worktree.directory, strategy: "rift" }]); + const result = await Effect.runPromise(f.execute("session_create", { + prompt: "Implement the change", title: "Implementation", directory: created.worktree.directory, + })); + assert.equal(result.status, "started"); + assert.equal(result.admitted, true); + assert.equal(result.session.directory, "/snapshots/new"); + assert.equal(result.session.agent, "worker"); + assert.deepEqual(result.session.model, model); + assert.equal(result.session.title, "Implementation"); + assert.deepEqual(f.calls.map((call) => call.name), ["worktree", "create", "progress", "prompt"]); + assert.deepEqual(f.calls[0]!.input, { location: f.sessions.get("ses_caller")!.location, name: "task", strategy: undefined, branch: undefined }); + assert.deepEqual(f.calls.at(-1)!.input, { sessionID: result.session.sessionID, text: "Implement the change", delivery: "steer" }); + assert.equal(f.calls.some((call) => call.name === "wait"), false); + }); + + test("creates directly in an existing worktree or checkout", async () => { + for (const directory of ["/repo", "/snapshots/task"]) { + const f = fixture(); + const result = await Effect.runPromise(f.execute("session_create", { prompt: "Review", directory })); + assert.equal(result.session.directory, directory); + assert.equal(f.calls.some((call) => call.name === "worktree"), false); + } + }); + + test("omitted directory uses the caller's location, including a worktree subdirectory", async () => { + const f = fixture(); + f.sessions.set("ses_caller", session("ses_caller", "/snapshots/task/src")); + const result = await Effect.runPromise(f.execute("session_create", { prompt: "Review" })); + assert.equal(result.session.directory, "/snapshots/task/src"); + assert.equal(f.calls.some((call) => call.name === "worktree"), false); + const checkout = await Effect.runPromise(f.execute("session_create", { directory: "/repo" })); + assert.equal(checkout.session.directory, "/repo"); + }); + + test("rejects the removed environment field rather than silently using the current directory", () => { + const f = fixture(); + assert.throws(() => f.execute("session_create", { environment: { type: "new_worktree" } })); + assert.deepEqual(f.calls, []); + }); + + test("rejects unmanaged directories, foreign callers, unknown agents and models before mutation", async () => { + const inputs = [ + { directory: "/arbitrary" }, + { agent: "missing" }, + { model: { providerID: "unknown", id: "unknown" } }, + { model: { ...model, variant: "unknown" } }, + ]; + for (const input of inputs) { + const f = fixture(); + await assert.rejects(Effect.runPromise(f.execute("session_create", { prompt: "Work", ...input }))); + assert.deepEqual(f.calls, []); + } + const f = fixture(); + f.sessions.set("ses_caller", session("ses_caller", "/foreign", "foreign")); + await assert.rejects(Effect.runPromise(f.execute("session_create", { prompt: "Work" })), /another OpenCode project/); + assert.deepEqual(f.calls, []); + }); + + test("passes an explicitly selected Git strategy and starting ref", async () => { + const f = fixture(); + await Effect.runPromise(f.execute("worktree_create", { strategy: "git", branch: "main" })); + assert.equal((f.calls[0]!.input as { strategy: string }).strategy, "git"); + assert.equal((f.calls[0]!.input as { branch: string }).branch, "main"); + }); + + test("preserves resources and returns recovery details when prompt admission fails", async () => { + const f = fixture(); + f.methods.prompt = () => Effect.fail(new Error("Provider unavailable")); + const result = await Effect.runPromise(f.execute("session_create", { prompt: "Work", directory: "/snapshots/task" })); + assert.equal(result.status, "partial_failure"); + assert.equal(result.phase, "prompt"); + assert.equal(result.admitted, false); + assert.equal(result.session.directory, "/snapshots/task"); + assert.ok(f.sessions.has(result.session.sessionID)); + assert.equal(result.requestedSessionID, result.session.sessionID); + }); + + test("reports session creation failure without mutating worktrees", async () => { + const f = fixture(); + f.methods.create = () => Effect.fail(new Error("Creation failed")); + const result = await Effect.runPromise(f.execute("session_create", { prompt: "Work", directory: "/snapshots/task" })); + assert.equal(result.phase, "session_create"); + assert.equal(result.session, undefined); + assert.match(result.requestedSessionID, /^ses_/); + assert.equal(result.directory, "/snapshots/task"); + assert.deepEqual(f.calls, []); + }); + + test("never prompts a session created in an unexpected location", async () => { + const f = fixture(); + f.methods.create = () => Effect.succeed(session("ses_wrong", "/unexpected")); + const result = await Effect.runPromise(f.execute("session_create", { prompt: "Work" })); + assert.equal(result.status, "partial_failure"); + assert.equal(result.session.sessionID, "ses_wrong"); + assert.equal(f.calls.some((call) => call.name === "prompt"), false); + }); + + test("supports follow-up queueing and explicit agent/model changes", async () => { + const f = fixture(); + const result = await Effect.runPromise(f.execute("session_send", { sessionID: "ses_target", prompt: "Next task", delivery: "queue", agent: "reviewer", model })); + assert.equal(result.admitted, true); + assert.deepEqual(f.calls.map((call) => call.name), ["switchAgent", "switchModel", "prompt"]); + assert.equal((f.calls.at(-1)!.input as { delivery: string }).delivery, "queue"); + }); + + test("rejects self-targeting and foreign sessions across lifecycle tools", async () => { + for (const sessionID of ["ses_caller", "ses_foreign"]) { + for (const [name, input] of [ + ["session_send", { sessionID, prompt: "Work" }], + ["session_wait", { targets: [{ sessionID }] }], + ["session_interrupt", { sessionID }], + ] as const) { + const f = fixture(); + await assert.rejects(Effect.runPromise(f.execute(name, input))); + assert.deepEqual(f.calls, []); + } + } + const f = fixture(); + await assert.rejects(Effect.runPromise(f.execute("session_read", { sessionID: "ses_foreign" })), /another OpenCode project/); + }); + + test("returns native interrupt results", async () => { + const f = fixture(); + const result = await Effect.runPromise(f.execute("session_interrupt", { sessionID: "ses_target" })); + assert.deepEqual(result, { sessionID: "ses_target", interrupted: true }); + assert.deepEqual(f.calls[0]!.input, { sessionID: "ses_target", continue: false }); + }); + + test("native wait completes and releases losing waits", async () => { + const f = fixture(); + const released: string[] = []; + const started = Deferred.makeUnsafe(); + f.methods.wait = ({ sessionID }) => sessionID === "ses_target" + ? Deferred.await(started) + : Effect.acquireUseRelease(Effect.void, + () => Effect.gen(function* () { yield* Effect.yieldNow; yield* Deferred.succeed(started, undefined); yield* Effect.never; }), + () => Effect.sync(() => { released.push(sessionID); }), + ); + const result = await Effect.runPromise(f.execute("session_wait", { targets: [{ sessionID: "ses_target" }, { sessionID: "ses_other" }] })); + assert.equal(result.status, "completed"); + assert.equal(result.completed.session.sessionID, "ses_target"); + assert.deepEqual(released, ["ses_other"]); + }); + + test("native wait timeouts are capped and release subscriptions without interrupting sessions", async () => { + const f = fixture(); + f.config.adapters.opencode.navigator.maxWaitMs = 5; + let released = false; + f.methods.wait = () => Effect.never.pipe(Effect.ensuring(Effect.sync(() => { released = true; }))); + const result = await Effect.runPromise(f.execute("session_wait", { targets: [{ sessionID: "ses_target" }], timeoutMs: 100000 })); + assert.equal(result.status, "timeout"); + assert.equal(released, true); + assert.equal(f.calls.some((call) => call.name === "interrupt"), false); + }); + + test("cancelling the tool releases native waits", async () => { + const f = fixture(); + let released = false; + const started = Deferred.makeUnsafe(); + f.methods.wait = () => Effect.gen(function* () { yield* Deferred.succeed(started, undefined); yield* Effect.never; }).pipe( + Effect.ensuring(Effect.sync(() => { released = true; })), + ); + await Effect.runPromise(Effect.gen(function* () { + const fiber = yield* Effect.forkChild(f.execute("session_wait", { targets: [{ sessionID: "ses_target" }] })); + yield* Deferred.await(started); + yield* Fiber.interrupt(fiber); + })); + assert.equal(released, true); + assert.equal(f.calls.some((call) => call.name === "interrupt"), false); + }); + + test("session reads bound current context and optional tool output", () => { + const f = fixture(); + const messages = Schema.decodeUnknownSync(Schema.Array(SessionMessage.Info))([ + { type: "user", id: "msg_old", time: { created: 1 }, text: "old" }, + { type: "assistant", id: "msg_latest", time: { created: 2 }, agent: "worker", model, content: [ + { type: "text", text: "x".repeat(100) }, + { type: "tool", id: "tool", name: "shell", time: { created: 2 }, state: { status: "completed", input: {}, content: [{ type: "text", text: "y".repeat(100) }] } }, + ] }, + ]); + f.config.adapters.opencode.navigator.maxOutputCharsPerItem = 10; + const result = summarizeV2Messages(messages, f.config.adapters.opencode.navigator, { turnLimit: 1, includeOutputs: true }); + assert.equal(result.messages.length, 1); + assert.ok(JSON.stringify(result.messages).includes("yyyyyyyyyy")); + assert.ok(!JSON.stringify(result.messages).includes("yyyyyyyyyyy")); + assert.equal(result.truncation.truncated, true); + const withoutOutputs = summarizeV2Messages(messages, f.config.adapters.opencode.navigator); + assert.ok(!JSON.stringify(withoutOutputs.messages).includes("yyyy")); + f.config.adapters.opencode.navigator.maxReadChars = 200; + assert.ok(JSON.stringify(summarizeV2Messages(messages, f.config.adapters.opencode.navigator).messages).length <= 200); + }); +}); diff --git a/packages/opencode/test/navigator.test.ts b/packages/opencode/test/navigator.test.ts index 7b5fb14..0fe3118 100644 --- a/packages/opencode/test/navigator.test.ts +++ b/packages/opencode/test/navigator.test.ts @@ -98,6 +98,37 @@ function context(sessionID = "caller", abort = new AbortController().signal) { } describe("Kompass Navigator", () => { + test("defaults to the caller directory and creates idle sessions on both protocols", async () => { + for (const protocol of ["v1", "v2"] as const) { + let prompts = 0; + const client = createClient({ + session: { + get: async ({ sessionID }: any) => response({ data: session(sessionID, "/repo-worktree/src") }), + prompt: async () => { prompts++; return response({ data: {} }); }, + }, + legacySession: { promptAsync: async () => { prompts++; return response(undefined); } }, + }); + const result = JSON.parse(await tools(client, navigatorConfig, protocol).session_create.execute({}, context()) as string); + assert.equal(result.directory, "/repo-worktree/src"); + assert.equal(result.status, "created"); + assert.equal(result.admitted, false); + assert.equal(prompts, 0); + const checkout = JSON.parse(await tools(client, navigatorConfig, protocol).session_create.execute({ directory: "/repo" }, context()) as string); + assert.equal(checkout.directory, "/repo"); + } + }); + + test("legacy idle sessions reject agent/model options that the API cannot persist", async () => { + const navigator = tools(createClient(), navigatorConfig, "v1"); + await assert.rejects(navigator.session_create.execute({ agent: "worker" }, context()), /when prompting/); + await assert.rejects(navigator.session_create.execute({ environment: { type: "new_worktree" } }, context()), /no longer supported/); + }); + test("creates standalone worktrees without creating a session", async () => { + const client = createClient({ session: { create: async () => { throw new Error("Must not create a session"); } } }); + const output = JSON.parse(await tools(client).worktree_create.execute({ name: "task" }, context()) as string); + assert.equal(output.status, "created"); + assert.equal(output.worktree.directory, "/repo-new"); + }); test("reserves Navigator tools for explicit native-session workflows", () => { for (const definition of Object.values(tools())) { assert.match(definition.description, /explicitly asks/); @@ -247,7 +278,7 @@ describe("Kompass Navigator", () => { await assert.rejects( (tools().session_create as any).execute({ prompt: "work", - environment: { type: "existing_worktree", directory: "/tmp/arbitrary" }, + directory: "/tmp/arbitrary", }, context()), /not a managed OpenCode workspace/, ); @@ -277,7 +308,7 @@ describe("Kompass Navigator", () => { }); const output = JSON.parse(await (tools(client).session_create as any).execute({ prompt: "implement it", - environment: { type: "existing_worktree", directory: "/repo-worktree" }, + directory: "/repo-worktree", }, context())); assert.equal(output.directory, "/repo-worktree"); @@ -305,7 +336,7 @@ describe("Kompass Navigator", () => { await (tools(client).session_create as any).execute({ prompt: "review it", - environment: { type: "checkout" }, + directory: "/repo", }, context()); assert.equal(creates[0].agent, "reviewer"); @@ -317,7 +348,7 @@ describe("Kompass Navigator", () => { }); }); - test("validates an unknown V2 agent in a newly created worktree and rolls it back", async () => { + test("validates an unknown agent without mutating worktrees", async () => { let worktreeCreates = 0; let worktreeRemoves = 0; let sessionCreates = 0; @@ -334,12 +365,12 @@ describe("Kompass Navigator", () => { (tools(client).session_create as any).execute({ prompt: "review it", agent: "review", - environment: { type: "new_worktree" }, + directory: "/repo-worktree", }, context()), /Unknown OpenCode agent "review".*reviewer/, ); - assert.equal(worktreeCreates, 1); - assert.equal(worktreeRemoves, 1); + assert.equal(worktreeCreates, 0); + assert.equal(worktreeRemoves, 0); assert.equal(sessionCreates, 0); }); @@ -352,12 +383,12 @@ describe("Kompass Navigator", () => { await assert.rejects((tools(client).session_create as any).execute({ prompt: "work", model: { providerID: "openai", modelID: "missing" }, - environment: { type: "checkout" }, + directory: "/repo", }, context()), /Unknown or disabled OpenCode model.*Available openai models/); await assert.rejects((tools(client).session_create as any).execute({ prompt: "work", model: { providerID: "openai", modelID: "gpt-5.6-sol", variant: "missing" }, - environment: { type: "checkout" }, + directory: "/repo", }, context()), /Unknown variant.*Available variants: xhigh/); assert.equal(sessionCreates, 0); }); @@ -462,11 +493,10 @@ describe("Kompass Navigator", () => { }); test("returns new worktree details", async () => { - const output = JSON.parse(await (tools().session_create as any).execute({ - prompt: "implement it", - environment: { type: "new_worktree", name: "feature" }, + const output = JSON.parse(await (tools().worktree_create as any).execute({ + name: "feature", }, context())); - assert.deepEqual(output.worktree, { created: true, type: "worktree", name: "new", branch: "new" }); + assert.deepEqual(output.worktree, { directory: "/repo-new", type: "worktree", name: "new", branch: "new" }); }); test("prefers Rift workspace adapter for new workspaces when available", async () => { @@ -499,16 +529,15 @@ describe("Kompass Navigator", () => { }, }; - const output = JSON.parse(await (tools(client).session_create as any).execute({ - prompt: "implement it", - environment: { type: "new_worktree", name: "Parser Fix" }, + const output = JSON.parse(await (tools(client).worktree_create as any).execute({ + name: "Parser Fix", }, context())); assert.deepEqual(workspaceCreates, [{ directory: "/repo", type: "rift", extra: { name: "Parser Fix" } }]); - assert.deepEqual(sessionCreates[0].location, { directory: "/repo-rift", workspaceID: "wrk_rift" }); - assert.equal(output.workspaceID, "wrk_rift"); + assert.deepEqual(sessionCreates, []); assert.deepEqual(output.worktree, { - created: true, + directory: "/repo-rift", + projectID: "project-1", type: "rift", name: "parser-fix", id: "wrk_rift", @@ -545,7 +574,7 @@ describe("Kompass Navigator", () => { await (tools(client).session_create as any).execute({ prompt: "continue", - environment: { type: "existing_worktree", directory: "/repo-rift" }, + directory: "/repo-rift", }, context()); assert.deepEqual(creates[0].location, { directory: "/repo-rift", workspaceID: "wrk_existing" }); @@ -566,14 +595,13 @@ describe("Kompass Navigator", () => { workspace: { adapter: { list: async () => response([{ type: "rift" }]) }, syncList: async () => response(undefined), - list: async () => response([]), - create: async () => response({ + list: async () => response([{ id: "wrk_expected", type: "rift", name: "new", directory: "/repo-rift", projectID: "project-1", - }), + }]), remove: async () => { removes += 1; return response(undefined); }, }, }; @@ -581,12 +609,12 @@ describe("Kompass Navigator", () => { await assert.rejects( (tools(client).session_create as any).execute({ prompt: "work", - environment: { type: "new_worktree" }, + directory: "/repo-rift", }, context()), - /workspace ID wrk_wrong; expected wrk_expected.*not prompted.*rolled back/, + /workspace ID wrk_wrong; expected wrk_expected.*not prompted/, ); assert.equal(prompts, 0); - assert.equal(removes, 1); + assert.equal(removes, 0); }); test("does not remove an existing Rift after returned location validation fails", async () => { @@ -611,7 +639,7 @@ describe("Kompass Navigator", () => { await assert.rejects((tools(client).session_create as any).execute({ prompt: "work", - environment: { type: "existing_worktree", directory: "/repo-rift" }, + directory: "/repo-rift", }, context()), /uses directory \/wrong.*not prompted/); assert.equal(removes, 0); }); @@ -621,13 +649,13 @@ describe("Kompass Navigator", () => { await assert.rejects( (tools(client).session_create as any).execute({ prompt: "implement it", - environment: { type: "new_worktree" }, + directory: "/repo-worktree", }, context()), - /Session created: created.*Worktree created: new \(\/repo-new\)/, + /Session created: created/, ); }); - test("reports a worktree created before a session failure", async () => { + test("session creation failure preserves existing worktrees", async () => { const removes: any[] = []; const client = createClient({ worktree: { remove: async (args: any) => { removes.push(args); return response(true); } }, @@ -636,11 +664,11 @@ describe("Kompass Navigator", () => { await assert.rejects( (tools(client).session_create as any).execute({ prompt: "implement it", - environment: { type: "new_worktree" }, + directory: "/repo-worktree", }, context()), - /Workspace \/repo-new was rolled back/, + /session creation failed.*create failed/, ); - assert.equal(removes[0].worktreeRemoveInput.directory, "/repo-new"); + assert.deepEqual(removes, []); }); test("discovers a worktree left behind by a failed native create", async () => { @@ -655,15 +683,14 @@ describe("Kompass Navigator", () => { }, }); await assert.rejects( - (tools(client).session_create as any).execute({ - prompt: "implement it", - environment: { type: "new_worktree", startCommand: "false" }, + (tools(client).worktree_create as any).execute({ + startCommand: "false", }, context()), - /partially created workspace was removed/, + /start command failed/, ); }); - test("rejects a foreign Rift workspace before creating a session and rolls it back", async () => { + test("reports a foreign Rift workspace and preserves it for recovery", async () => { const removes: any[] = []; let sessionCreates = 0; const client = createClient({ @@ -687,14 +714,11 @@ describe("Kompass Navigator", () => { }; await assert.rejects( - (tools(client).session_create as any).execute({ - prompt: "implement it", - environment: { type: "new_worktree" }, - }, context()), - /registered workspace wrk_foreign to project project-2; expected project-1.*partially created workspace was removed/, + (tools(client).worktree_create as any).execute({}, context()), + /another project.*preserved for recovery/, ); assert.equal(sessionCreates, 0); - assert.deepEqual(removes, [{ id: "wrk_foreign", directory: "/repo" }]); + assert.deepEqual(removes, []); }); test("rejects self send, wait, and interrupt", async () => { diff --git a/packages/opencode/test/plugin-entry.test.ts b/packages/opencode/test/plugin-entry.test.ts index b6b234b..7ff3d76 100644 --- a/packages/opencode/test/plugin-entry.test.ts +++ b/packages/opencode/test/plugin-entry.test.ts @@ -17,8 +17,8 @@ describe("plugin entry", () => { ); assert.equal(mod.default, mod.OpenCodeCompassPlugin); assert.equal(mod.default.server, mod.OpenCodeCompassPluginV1); - assert.equal(mod.default.setup, mod.setupOpenCodeV2); + assert.equal(mod.default.effect, mod.OpenCodeCompassPluginV2.effect); assert.equal(mod.default.id, "kompass"); - assert.equal(mod.OpenCodeCompassPluginV2.setup, mod.setupOpenCodeV2); + assert.equal(typeof mod.OpenCodeCompassPluginV2.effect, "function"); }); }); diff --git a/packages/opencode/test/rift-worktree.test.ts b/packages/opencode/test/rift-worktree.test.ts index 5d29922..cc9313f 100644 --- a/packages/opencode/test/rift-worktree.test.ts +++ b/packages/opencode/test/rift-worktree.test.ts @@ -5,12 +5,12 @@ import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; -import { Worktree } from "@opencode/plugin"; +import { Worktree } from "@opencode/plugin/effect"; +import { Deferred, Effect, Fiber } from "effect"; import { createRiftWorktreeStrategy } from "../rift-worktree.ts"; import type { RiftBackend } from "../rift.ts"; const execFileAsync = promisify(execFile); -const context = () => ({ signal: new AbortController().signal }); function backend(overrides: Partial = {}): RiftBackend { return { @@ -26,11 +26,11 @@ function backend(overrides: Partial = {}): RiftBackend { describe("Rift V2 worktree strategy", () => { test("uses OpenCode's source and name while letting Rift choose the destination", async () => { const calls: unknown[] = []; - const strategy = createRiftWorktreeStrategy(async () => backend({ + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ init(options) { calls.push(options); return null; }, create(options) { calls.push(options); return "/snapshots/actual"; }, - })); - assert.deepEqual(await strategy.create({ sourceDirectory: "/source", directory: "/custom/task" }, context()), { + }))); + assert.deepEqual(await Effect.runPromise(strategy.create({ sourceDirectory: "/source", directory: "/custom/task" })), { directory: "/snapshots/actual", }); assert.deepEqual(calls, [ @@ -41,14 +41,14 @@ describe("Rift V2 worktree strategy", () => { test("suffixes name collisions in Rift's own storage", async () => { const names: Array = []; - const strategy = createRiftWorktreeStrategy(async () => backend({ + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ create(options) { names.push(options?.name); if (names.length === 1) throw Object.assign(new Error("Snapshot exists"), { code: "already_exists" }); return `/projects/.rifts/source/${options?.name}`; }, - })); - assert.deepEqual(await strategy.create({ sourceDirectory: "/source", directory: "/suggested/task" }, context()), { + }))); + assert.deepEqual(await Effect.runPromise(strategy.create({ sourceDirectory: "/source", directory: "/suggested/task" })), { directory: "/projects/.rifts/source/task-2", }); assert.deepEqual(names, ["task", "task-2"]); @@ -56,45 +56,60 @@ describe("Rift V2 worktree strategy", () => { test("suffixes a snapshot name that would target the source snapshot itself", async () => { const sourceDirectory = "/projects/.rifts/source/task"; - const strategy = createRiftWorktreeStrategy(async () => backend({ + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ create(options) { if (options?.name === "task") { throw Object.assign(new Error("Cannot copy into itself"), { code: "inside_source", path: sourceDirectory }); } return `/projects/.rifts/source/${options?.name}`; }, - })); - assert.deepEqual(await strategy.create({ sourceDirectory, directory: "/suggested/task" }, context()), { + }))); + assert.deepEqual(await Effect.runPromise(strategy.create({ sourceDirectory, directory: "/suggested/task" })), { directory: "/projects/.rifts/source/task-2", }); }); - test("rejects starting refs and cancelled operations before loading Rift", async () => { - const strategy = createRiftWorktreeStrategy(async () => { throw new Error("Must not load"); }); - await assert.rejects(strategy.create({ sourceDirectory: "/source", directory: "/task", branch: "main" }, context()), + test("rejects starting refs before loading Rift", async () => { + const strategy = createRiftWorktreeStrategy(Effect.die("Must not load")); + await assert.rejects(Effect.runPromise(strategy.create({ sourceDirectory: "/source", directory: "/task", branch: "main" })), (error) => error instanceof Worktree.OperationError && /starting branch or ref/.test(error.message)); - const cancelled = { signal: AbortSignal.abort() }; - await assert.rejects(strategy.create({ sourceDirectory: "/source", directory: "/task" }, cancelled), { name: "AbortError" }); - await assert.rejects(strategy.list("/source", cancelled), { name: "AbortError" }); - await assert.rejects(strategy.remove({ directory: "/task", force: true }, cancelled), { name: "AbortError" }); + }); + + test("interruption releases backend acquisition without performing native operations", async () => { + for (const operation of ["create", "list", "remove"] as const) { + const started = Deferred.makeUnsafe(); + let released = false; + const strategy = createRiftWorktreeStrategy(Effect.gen(function* () { + yield* Deferred.succeed(started, undefined); + return yield* Effect.never; + }).pipe(Effect.ensuring(Effect.sync(() => { released = true; })))); + const effect = operation === "create" ? strategy.create({ sourceDirectory: "/source", directory: "/task" }) + : operation === "list" ? strategy.list("/source") : strategy.remove({ directory: "/task", force: true }); + await Effect.runPromise(Effect.gen(function* () { + const fiber = yield* Effect.forkChild(effect); + yield* Deferred.await(started); + yield* Fiber.interrupt(fiber); + })); + assert.equal(released, true); + } }); test("propagates creation errors without retrying", async () => { let attempts = 0; const failure = new Error("copy-on-write unavailable"); - const strategy = createRiftWorktreeStrategy(async () => backend({ create() { attempts++; throw failure; } })); - await assert.rejects(strategy.create({ sourceDirectory: "/source", directory: "/task" }, context()), (error) => error === failure); + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ create() { attempts++; throw failure; } }))); + await assert.rejects(Effect.runPromise(strategy.create({ sourceDirectory: "/source", directory: "/task" })), (error) => error === failure); assert.equal(attempts, 1); }); test("uninitialized roots have no inventory; registry errors are surfaced", async () => { - const strategy = createRiftWorktreeStrategy(async () => backend({ + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ ancestors() { throw Object.assign(new Error("Not initialized"), { code: "workspace_not_initialized" }); }, - })); - assert.deepEqual(await strategy.list("/source", context()), []); + }))); + assert.deepEqual(await Effect.runPromise(strategy.list("/source")), []); const failure = new Error("registry damaged"); - const broken = createRiftWorktreeStrategy(async () => backend({ ancestors() { throw failure; } })); - await assert.rejects(broken.list("/source", context()), (error) => error === failure); + const broken = createRiftWorktreeStrategy(Effect.succeed(backend({ ancestors() { throw failure; } }))); + await assert.rejects(Effect.runPromise(broken.list("/source")), (error) => error === failure); }); test("discovers siblings and descendants from a snapshot, skipping missing entries", async (t) => { @@ -105,11 +120,11 @@ describe("Rift V2 worktree strategy", () => { const second = path.join(root, "second"); const nested = path.join(root, "nested"); await Promise.all([source, first, second, nested].map((directory) => mkdir(directory))); - const strategy = createRiftWorktreeStrategy(async () => backend({ + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ ancestors: () => [source], list: ({ of } = {}) => of === source ? [first, second, first, path.join(root, "missing")] : of === first ? [nested] : [], - })); - assert.deepEqual(await strategy.list(first, context()), [ + }))); + assert.deepEqual(await Effect.runPromise(strategy.list(first)), [ { directory: source, type: "root" }, { directory: first, type: "worktree" }, { directory: second, type: "worktree" }, @@ -124,20 +139,20 @@ describe("Rift V2 worktree strategy", () => { await mkdir(child); const removed: string[] = []; let children: string[] = []; - const strategy = createRiftWorktreeStrategy(async () => backend({ + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ ancestors: ({ of } = {}) => of === root ? [] : [root], list: ({ of } = {}) => of === root ? [child] : children, remove: (options) => { removed.push(options!.at!); }, - })); - await assert.rejects(strategy.remove({ directory: root, force: true }, context()), /Not a Rift snapshot root/); + }))); + await assert.rejects(Effect.runPromise(strategy.remove({ directory: root, force: true })), /Not a Rift snapshot root/); const subdirectory = path.join(child, "subdirectory"); await mkdir(subdirectory); - await assert.rejects(strategy.remove({ directory: subdirectory, force: true }, context()), /Not a Rift snapshot root/); + await assert.rejects(Effect.runPromise(strategy.remove({ directory: subdirectory, force: true })), /Not a Rift snapshot root/); children = ["descendant"]; - await assert.rejects(strategy.remove({ directory: child, force: true }, context()), /child snapshots first/); + await assert.rejects(Effect.runPromise(strategy.remove({ directory: child, force: true })), /child snapshots first/); assert.deepEqual(removed, []); children = []; - await strategy.remove({ directory: child, force: true }, context()); + await Effect.runPromise(strategy.remove({ directory: child, force: true })); assert.deepEqual(removed, [child]); }); @@ -152,18 +167,18 @@ describe("Rift V2 worktree strategy", () => { await writeFile(path.join(snapshot, ".rift"), "rift-marker"); await writeFile(path.join(snapshot, "user.txt"), "uncommitted"); let removed = 0; - const strategy = createRiftWorktreeStrategy(async () => backend({ + const strategy = createRiftWorktreeStrategy(Effect.succeed(backend({ ancestors: () => [root], list: ({ of } = {}) => of === root ? [snapshot] : [], remove: () => { removed++; }, - })); - await assert.rejects(strategy.remove({ directory: snapshot, force: false }, context()), + }))); + await assert.rejects(Effect.runPromise(strategy.remove({ directory: snapshot, force: false })), (error) => error instanceof Worktree.OperationError && error.forceRequired === true); assert.equal(removed, 0); - await strategy.remove({ directory: snapshot, force: true }, context()); + await Effect.runPromise(strategy.remove({ directory: snapshot, force: true })); assert.equal(removed, 1); await rm(path.join(snapshot, "user.txt")); - await strategy.remove({ directory: snapshot, force: false }, context()); + await Effect.runPromise(strategy.remove({ directory: snapshot, force: false })); assert.equal(removed, 2); }); }); diff --git a/packages/opencode/test/v2-plugin.test.ts b/packages/opencode/test/v2-plugin.test.ts index 70d7d62..3caf82a 100644 --- a/packages/opencode/test/v2-plugin.test.ts +++ b/packages/opencode/test/v2-plugin.test.ts @@ -1,128 +1,122 @@ import { describe, test } from "node:test"; import assert from "node:assert/strict"; - -import { - OpenCodeCompassPluginV2, - setupOpenCodeV2, -} from "../index.ts"; +import { Agent, Location, type Plugin } from "@opencode/plugin/effect"; +import type { CommandDefinition } from "@opencode/plugin/effect/command"; +import type { Transform } from "@opencode/plugin/effect/registration"; +import type { WorktreeDefinition } from "@opencode/plugin/effect/worktree"; +import { Session } from "@opencode/schema/session"; +import { Tool } from "@opencode/schema/tool"; +import { Effect, Schema } from "effect"; +import { z } from "zod"; +import { OpenCodeCompassPluginV2, setupOpenCodeV2 } from "../index.ts"; import { loadResolvedCommands } from "../cache.ts"; -import type { WorktreeDefinition } from "@opencode/plugin/promise/worktree"; +import { createOpenCodeV2Tools } from "../v2.ts"; +import { mergeWithDefaults } from "../../core/index.ts"; + +function fixture() { + const agents = new Map(); + const commands = new Map(); + const tools = new Map(); + const worktrees: WorktreeDefinition[] = []; + const prompts: Array> = []; + const createdSessions: Array> = []; + const switchedAgents: string[] = []; + const disposed: string[] = []; + function transform(name: string, editor: Editor): Transform { + return (callback) => Effect.gen(function* () { + callback(editor); + let active = true; + const dispose = Effect.sync(() => { if (active) disposed.push(name); active = false; }); + yield* Effect.addFinalizer(() => dispose); + return { dispose }; + }); + } + const ctx = { + location: Schema.decodeUnknownSync(Location.Info)({ directory: process.cwd(), project: { id: "project", directory: process.cwd(), canonical: process.cwd() } }), + agent: { transform: transform("agent", { + update: (id: string, update: (agent: Agent.Info) => void) => { + const agent = agents.get(id) ?? Agent.Info.default(Agent.ID.make(id)); + update(agent); + agents.set(id, agent); + }, + }) }, + command: { transform: transform("command", { add: (command: CommandDefinition) => commands.set(command.name, command) }) }, + tool: { transform: transform("tool", { add: (tool: Tool.Info) => tools.set(tool.name, tool) }) }, + worktree: { transform: transform("worktree", { add: (strategy: WorktreeDefinition) => worktrees.push(strategy) }) }, + session: { + create: (input: Record) => Effect.sync(() => { + createdSessions.push(input); + return { id: Session.ID.make("ses_child") }; + }), + switchAgent: ({ agent }: { agent: string }) => Effect.sync(() => { switchedAgents.push(agent); }), + prompt: (input: Record) => Effect.sync(() => { prompts.push(input); }), + }, + }; + return { ctx, context: ctx as unknown as Plugin.Context, agents, commands, tools, worktrees, prompts, createdSessions, switchedAgents, disposed }; +} describe("OpenCode v2 plugin", () => { - test("exports the v2 plugin definition", () => { + test("exports the native Effect setup directly", () => { assert.equal(OpenCodeCompassPluginV2.id, "kompass"); - assert.equal(OpenCodeCompassPluginV2.setup, setupOpenCodeV2); + assert.equal(OpenCodeCompassPluginV2.effect, setupOpenCodeV2); }); - test("registers location-scoped agents, commands, and tools", async () => { - const agents: string[] = []; - type Command = { name: string; execute(input: { sessionID: string; prompt: { text: string; files?: unknown[] }; delivery: string }): Promise }; - const commands = new Map(); - const prompts: Array> = []; - const switchedAgents: string[] = []; - const createdSessions: Array> = []; - const tools = new Map(); - const disposed: string[] = []; - const worktrees: WorktreeDefinition[] = []; - const availableAgents = new Map>(); - const context = { - location: { directory: process.cwd() }, - worktree: { - transform: async (transform: (draft: { add(strategy: WorktreeDefinition): void }) => void) => { - transform({ add: (strategy) => { worktrees.push(strategy); } }); - return { dispose: async () => { disposed.push("worktree"); } }; - }, - }, - agent: { - transform: async (transform: (draft: unknown) => void) => { - transform({ - update(name: string, update: (agent: Record) => void) { - agents.push(name); - const agent = availableAgents.get(name) ?? { id: name, permissions: [] }; - update(agent); - availableAgents.set(name, agent); - }, - }); - return { dispose: async () => { disposed.push("agent"); } }; - }, - reload: async () => {}, - }, - command: { - transform: async (transform: (draft: unknown) => void) => { - transform({ - add(command: Command) { - commands.set(command.name, command); - }, - }); - return { dispose: async () => { disposed.push("command"); } }; - }, - reload: async () => {}, - }, - tool: { - transform: async (transform: (draft: unknown) => void) => { - transform({ - add(tool: { name: string; input: unknown }) { - tools.set(tool.name, tool); - }, - }); - return { dispose: async () => { disposed.push("tool"); } }; - }, - reload: async () => {}, - }, - session: { - create: async (input: Record) => { - createdSessions.push(input); - return { id: "child-1" }; - }, - switchAgent: async ({ agent }: { agent: string }) => { - switchedAgents.push(agent); - }, - prompt: async (input: Record) => { - prompts.push(input); - }, - }, - }; - - const cleanup = await setupOpenCodeV2(context as never); - assert.ok(agents.includes("worker")); - assert.ok(commands.has("dev")); - assert.deepEqual(worktrees.map((strategy) => strategy.id), ["rift"]); - assert.deepEqual([...tools.keys()].sort(), [ - "kompass_changes_load", - "kompass_pr_load", - "kompass_pr_load_review", - "kompass_pr_sync", - "kompass_ticket_load", - "kompass_ticket_sync", - ]); - assert.equal(typeof (tools.get("kompass_changes_load")?.input as any)?.["~standard"]?.validate, "function"); + test("registers agents, commands, all tools, and Rift on the host scope", async () => { + const f = fixture(); + await Effect.runPromise(Effect.scoped(Effect.gen(function* () { + yield* setupOpenCodeV2(f.context); + assert.ok(f.agents.has("worker")); + assert.ok(f.commands.has("dev")); + assert.deepEqual(f.worktrees.map((strategy) => strategy.id), ["rift"]); + assert.equal(Effect.isEffect(f.worktrees[0]!.list(process.cwd())), true); + assert.deepEqual([...f.tools.keys()].sort(), [ + "kompass_changes_load", "kompass_pr_load", "kompass_pr_load_review", "kompass_pr_sync", + "kompass_session_create", "kompass_session_interrupt", "kompass_session_read", "kompass_session_send", "kompass_session_wait", + "kompass_ticket_load", "kompass_ticket_sync", "kompass_worktree_create", "kompass_worktree_list", "kompass_worktree_remove", + ]); + assert.equal(typeof (f.tools.get("kompass_changes_load")!.input as z.ZodType)["~standard"].validate, "function"); + assert.deepEqual(f.disposed, []); + }))); + assert.deepEqual(f.disposed.sort(), ["agent", "command", "tool", "worktree"]); + }); + test("native command effects preserve arguments, attachments, delivery, and session selection", async () => { + const f = fixture(); const resolved = await loadResolvedCommands(process.cwd()); - assert.equal(commands.size, Object.keys(resolved).length); const text = "Keep literal $& $$ $1 $ARGUMENTS and !`printf should-not-run`"; const files = [{ uri: "file:///project/example.ts" }]; - await commands.get("dev")!.execute({ - sessionID: "session-1", - prompt: { text, files }, - delivery: "queue", - }); - assert.equal(prompts[0]?.text, resolved.dev!.template.replaceAll("$ARGUMENTS", () => text)); - assert.equal(prompts[0]?.files, files); - assert.equal(prompts[0]?.delivery, "queue"); - assert.equal(prompts[0]?.sessionID, process.env.CI ? "session-1" : "child-1"); - assert.equal(createdSessions.length, process.env.CI ? 0 : 1); - if (!process.env.CI) assert.equal(createdSessions[0]?.agent, "worker"); + const sessionID = Session.ID.make("ses_current"); + await Effect.runPromise(Effect.scoped(Effect.gen(function* () { + yield* setupOpenCodeV2(f.context); + assert.equal(f.commands.size, Object.keys(resolved).length); + yield* f.commands.get("dev")!.execute({ sessionID, prompt: { text, files }, delivery: "queue" }); + assert.equal(f.prompts[0]?.text, resolved.dev!.template.replaceAll("$ARGUMENTS", () => text)); + assert.equal(f.prompts[0]?.files, files); + assert.equal(f.prompts[0]?.delivery, "queue"); + assert.equal(f.prompts[0]?.sessionID, process.env.CI ? sessionID : "ses_child"); + assert.equal(f.createdSessions.length, process.env.CI ? 0 : 1); + if (!process.env.CI) assert.equal(f.createdSessions[0]?.agent, "worker"); + yield* f.commands.get("learn")!.execute({ sessionID, prompt: { text: "remember this" }, delivery: "steer" }); + assert.equal(f.switchedAgents.at(-1), "worker"); + assert.equal(f.prompts[1]?.sessionID, sessionID); + assert.equal(f.prompts[1]?.text, resolved.learn!.template.replaceAll("$ARGUMENTS", "remember this")); + }))); + }); - await commands.get("learn")!.execute({ - sessionID: "session-1", - prompt: { text: "remember this" }, - delivery: "steer", - }); - assert.equal(switchedAgents.at(-1), "worker"); - assert.equal(prompts[1]?.sessionID, "session-1"); - assert.equal(prompts[1]?.text, resolved.learn!.template.replaceAll("$ARGUMENTS", "remember this")); + test("failed setup releases prior native registrations", async () => { + const f = fixture(); + f.ctx.tool.transform = () => Effect.die(new Error("Registration failed")); + await assert.rejects(Effect.runPromise(Effect.scoped(setupOpenCodeV2(f.context))), /Registration failed/); + assert.deepEqual(f.disposed.sort(), ["agent", "command"]); + assert.deepEqual(f.worktrees, []); + }); - await cleanup(); - assert.deepEqual(disposed.sort(), ["agent", "command", "tool", "worktree"]); + test("shared core failures enter the native Tool.Error channel", async () => { + const tools = createOpenCodeV2Tools(process.cwd(), mergeWithDefaults({})); + const tool = tools.find((entry) => entry.name === "kompass_ticket_load")!; + const invocation = { sessionID: Session.ID.make("ses_current") } as Tool.Context; + const error = await Effect.runPromise(Effect.flip(tool.execute({ source: process.cwd() }, invocation))); + assert.ok(error instanceof Tool.Error); + assert.ok(error.message.length > 0); }); }); diff --git a/packages/opencode/v2.ts b/packages/opencode/v2.ts index 4edc2f2..9e7f41a 100644 --- a/packages/opencode/v2.ts +++ b/packages/opencode/v2.ts @@ -1,60 +1,48 @@ -import { Plugin } from "@opencode/plugin"; +import { Agent, Location, Plugin } from "@opencode/plugin/effect"; +import { Tool } from "@opencode/schema/tool"; +import { Effect } from "effect"; import { z } from "zod"; import { getEnabledToolNames, + type MergedKompassConfig, type ResolvedAgentDefinition, type ResolvedCommandDefinition, } from "../core/index.ts"; -import { - loadMergedKompassConfig, - loadResolvedAgents, - loadResolvedCommands, -} from "./cache.ts"; +import { loadMergedKompassConfig, loadResolvedAgents, loadResolvedCommands } from "./cache.ts"; import { createPluginLogger } from "./logging.ts"; import { createCoreOpenCodeToolDefinitions } from "./shared-tools.ts"; import { getConfiguredOpenCodeToolName } from "./tool-names.ts"; import { createRiftWorktreeStrategy } from "./rift-worktree.ts"; +import { createNavigatorV2Tools } from "./navigator-v2.ts"; -type PluginContext = Plugin.Context; -type ToolDraft = Parameters[0]>[0]; -type OpenCodeTool = Parameters[0]; - -function toOpenCodeTool( - name: string, - definition: ReturnType[string], - projectRoot: string, -): OpenCodeTool { - return { - name, - description: definition.description, - input: z.object(definition.args) as OpenCodeTool["input"], - options: { codemode: false }, - async execute(args) { - const result = await definition.execute(args as never, { - worktree: projectRoot, - directory: projectRoot, - } as never); - const content = typeof result === "string" ? result : result.output; - return { output: JSON.parse(content), content }; - }, - }; -} - -export async function createOpenCodeV2Tools(projectRoot: string): Promise { - const config = await loadMergedKompassConfig(projectRoot); +export function createOpenCodeV2Tools(projectRoot: string, config: MergedKompassConfig): Tool.Info[] { const definitions = createCoreOpenCodeToolDefinitions(config, projectRoot); - - return getEnabledToolNames(config.tools).flatMap((toolName) => { + return getEnabledToolNames(config.tools).flatMap((toolName): Tool.Info[] => { const definition = definitions[toolName]; if (!definition) return []; - const name = getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name); - return [toOpenCodeTool(name, definition, projectRoot)]; + return [{ + name: getConfiguredOpenCodeToolName(toolName, config.tools[toolName].name), + description: definition.description, + input: z.object(definition.args), + options: { codemode: false }, + execute: (args) => Effect.tryPromise({ + // The shared core uses Promises. Give each invocation its own shell and + // Effect's cancellation signal at that boundary. + try: async (signal) => { + const current = createCoreOpenCodeToolDefinitions(config, projectRoot, signal)[toolName]!; + const result = await current.execute(args, { worktree: projectRoot, directory: projectRoot }); + const content = typeof result === "string" ? result : result.output; + return { output: JSON.parse(content), content }; + }, + catch: (error) => new Tool.Error({ message: error instanceof Error ? error.message : String(error) }), + }), + }]; }); } function applyAgents( - draft: Parameters[0]>[0], + draft: Parameters[0]>[0], agents: Record, ) { for (const [name, definition] of Object.entries(agents)) { @@ -72,30 +60,26 @@ function applyAgents( } function applyCommands( - draft: Parameters[0]>[0], + draft: Parameters[0]>[0], commands: Record, - ctx: PluginContext, - projectRoot: string, + ctx: Plugin.Context, ) { for (const [name, definition] of Object.entries(commands)) { draft.add({ name, description: definition.description, - async execute({ sessionID, prompt, delivery }) { + execute: Effect.fn(`kompass.command.${name}`)(function* ({ sessionID, prompt, delivery }) { let targetSessionID = sessionID; if (definition.subtask) { - const session = await ctx.session.create({ - agent: definition.agent, - location: { - directory: projectRoot, - ...(ctx.location.workspaceID ? { workspaceID: ctx.location.workspaceID } : {}), - }, + const session = yield* ctx.session.create({ + agent: definition.agent ? Agent.ID.make(definition.agent) : undefined, + location: Location.Ref.make({ directory: ctx.location.directory, workspaceID: ctx.location.workspaceID }), }); - targetSessionID = session.id as typeof sessionID; + targetSessionID = session.id; } else if (definition.agent) { - await ctx.session.switchAgent({ sessionID, agent: definition.agent }); + yield* ctx.session.switchAgent({ sessionID, agent: Agent.ID.make(definition.agent) }); } - await ctx.session.prompt({ + yield* ctx.session.prompt({ ...prompt, sessionID: targetSessionID, text: definition.template.includes("$ARGUMENTS") @@ -103,39 +87,31 @@ function applyCommands( : `${definition.template}\n\n${prompt.text}`.trim(), delivery, }); - }, + }), }); } } -export async function setupOpenCodeV2(ctx: PluginContext) { +export const setupOpenCodeV2 = Effect.fn("kompass.setupV2")(function* (ctx: Plugin.Context) { const projectRoot = ctx.location.directory; - const logger = createPluginLogger(projectRoot); - const [agents, commands, tools] = await Promise.all([ - loadResolvedAgents(projectRoot), - loadResolvedCommands(projectRoot), - createOpenCodeV2Tools(projectRoot), - ]); - const agentRegistration = await ctx.agent.transform((draft) => applyAgents(draft, agents)); - const commandRegistration = await ctx.command.transform((draft) => applyCommands(draft, commands, ctx, projectRoot)); - const toolRegistration = await ctx.tool.transform((draft) => { + const [config, agents, commands] = yield* Effect.all([ + Effect.promise(() => loadMergedKompassConfig(projectRoot)), + Effect.promise(() => loadResolvedAgents(projectRoot)), + Effect.promise(() => loadResolvedCommands(projectRoot)), + ], { concurrency: "unbounded" }); + const tools = [...createOpenCodeV2Tools(projectRoot, config), ...createNavigatorV2Tools(ctx, config)]; + + // Native transforms attach their registrations to the plugin scope, including + // when setup fails partway through. The host owns their disposal. + yield* ctx.agent.transform((draft) => applyAgents(draft, agents)); + yield* ctx.command.transform((draft) => applyCommands(draft, commands, ctx)); + yield* ctx.tool.transform((draft) => { for (const tool of tools) draft.add(tool); }); - const worktreeRegistration = await ctx.worktree.transform((draft) => { + yield* ctx.worktree.transform((draft) => { draft.add(createRiftWorktreeStrategy()); }); - logger.info("Initialized Kompass v2 plugin", { directory: projectRoot, tools: tools.length }); - return async () => { - await Promise.all([ - agentRegistration.dispose(), - commandRegistration.dispose(), - toolRegistration.dispose(), - worktreeRegistration.dispose(), - ]); - }; -} - -export const OpenCodeCompassPluginV2 = Plugin.define({ - id: "kompass", - setup: setupOpenCodeV2, + createPluginLogger(projectRoot).info("Initialized Kompass v2 plugin", { directory: projectRoot, tools: tools.length }); }); + +export const OpenCodeCompassPluginV2 = Plugin.define({ id: "kompass", effect: setupOpenCodeV2 }); diff --git a/packages/web/src/components/CommandShowcase.astro b/packages/web/src/components/CommandShowcase.astro index 08ce90c..f9587c2 100644 --- a/packages/web/src/components/CommandShowcase.astro +++ b/packages/web/src/components/CommandShowcase.astro @@ -395,7 +395,7 @@ const scenarios: CommandScenario[] = [ id: 'dev', label: '/dev', command: '/dev update the command docs', - agentName: 'Navigator', + agentName: 'Worker', task: 'Implement a request and prepare a PR handoff', group: 'orchestration', steps: [ diff --git a/packages/web/src/content/docs/docs/adapters/opencode.mdx b/packages/web/src/content/docs/docs/adapters/opencode.mdx index 75bd347..a3dc2bb 100644 --- a/packages/web/src/content/docs/docs/adapters/opencode.mdx +++ b/packages/web/src/content/docs/docs/adapters/opencode.mdx @@ -18,7 +18,7 @@ Inside OpenCode, the adapter exposes: - Kompass commands - Kompass agents - Kompass tools -- Kompass Navigator session and worktree orchestration through the OpenCode 1 plugin API +- Kompass Navigator session orchestration on both OpenCode versions, with eight native V2 tools - a native Rift worktree strategy for OpenCode 2 - project override loading from local config files @@ -38,7 +38,7 @@ You can use overrides to: ## OpenCode 2 worktrees -Kompass registers `rift` through V2's native `ctx.worktree.transform` API. Registration selects it as the location's default, so OpenCode's own worktree actions use Rift. The adapter targets `@opencode/plugin` beta `19381`, including support for a strategy's returned directory. +Kompass registers `rift` through V2's native `ctx.worktree.transform` API. Registration selects it as the location's default for calls that omit a strategy. Desktop's current New worktree button explicitly selects Git; use Kompass's `worktree_create` tool for the registered default, then `session_create` to start work in the returned directory. The adapter targets `@opencode/plugin` beta `19381`, including support for a strategy's returned directory. Load Kompass in OpenCode's `opencode.jsonc`; no destination setting is needed: @@ -56,9 +56,33 @@ Load Kompass in OpenCode's `opencode.jsonc`; no destination setting is needed: The bundled `rift-snapshot` native bindings load on first use and require a supported copy-on-write filesystem. Native calls are synchronous and cannot be interrupted mid-operation; the adapter checks cancellation between calls. The V2 strategy is independent of the V1 Navigator settings. -## Kompass Navigator +## OpenCode 2 session orchestration -Navigator is available through the OpenCode 1 plugin API for explicitly requested native-session and managed-worktree workflows. Its eight tools have not been ported to the OpenCode 2 plugin API; V2 uses the native Rift strategy above. Navigator is not a subagent mechanism; ordinary delegation should use OpenCode's built-in `task` tool. Navigator follows OpenCode Desktop's protocol detection so session creation, prompts, reads, status, and interrupts stay on the same compatible API in the current checkout or managed Git worktrees. The V1 Navigator adapter polls the active-session API locally when using the V2 session protocol. It requires OpenCode `1.17.12` or newer and is enabled by default on OpenCode 1. +V2 registers eight Navigator tools: `worktree_create`, `worktree_list`, `worktree_remove`, `session_create`, `session_read`, `session_send`, `session_wait`, and `session_interrupt`. Default names have the `kompass_` prefix. The V2 plugin uses native Effect setup, commands, tools, and Rift operations. Registrations belong to the host's plugin scope. Shared Promise-based core tools run at an explicit Effect boundary that passes cancellation to their shell commands. + +Ask the agent to create a worktree and start work there. It calls `kompass_worktree_create` with `{ "name": "parser-fix" }`, then passes the returned directory to `kompass_session_create`: + +```json +{ + "prompt": "Implement the parser fix and run tests.", + "title": "Parser fix", + "directory": "/projects/.rifts/repo/parser-fix" +} +``` + +Session creation uses the supplied directory or defaults to the calling session's current location. It inherits the caller's agent/model/variant unless overridden and admits the optional prompt. It returns the session ID and directory immediately. `session_send` uses the target session's stored location and accepts `delivery: "steer"` or `"queue"`. Reads return bounded current model context. Waits use OpenCode's native wait, race up to eight targets, and release subscriptions on timeout or cancellation without interrupting the target sessions. + +Worktree creation is always a separate call to `worktree_create({ name?, strategy?, branch? })`. V2 `session_create` has no environment options and never creates or removes a worktree. Omit `prompt` to create an idle session. `worktree_remove({ directory, force? })` uses native removal and does not delete sessions. It protects the checkout and calling location. V2 cannot discover other active sessions, so finish known work before removal. + +Navigator follows the existing feature toggle and tool aliases/disables. `maxReadChars`, `maxOutputCharsPerItem`, and `maxWaitMs` apply. V2 `timeoutMs` is positive and capped by `maxWaitMs`. Session listing and `maxConcurrentSessions` remain V1-only because V2 plugins lack session discovery. Known session IDs still work after reloads. Partial failures preserve resources and return recovery details. + +See the [Navigator tool reference](/docs/reference/tools/navigator/) for V2 arguments and examples. + +## OpenCode 1 Navigator + +V1 session creation also accepts optional `directory` and `prompt`. Worktree creation is a separate call; the `environment` argument has been removed. With no directory, creation uses the calling session's current location. With no prompt, it creates an idle session. The legacy session protocol cannot persist agent/model selection before prompting; provide those overrides when sending the first prompt instead. + +The OpenCode 1 adapter exposes nine Navigator tools, including standalone worktree creation. Ordinary delegation uses OpenCode's built-in `task` tool. Navigator follows Desktop's protocol detection so session operations stay on one compatible API. The V1 adapter polls the active-session API locally when using the V2 session protocol. It requires OpenCode `1.17.12` or newer and is enabled by default. ```jsonc { @@ -80,11 +104,11 @@ Navigator is available through the OpenCode 1 plugin API for explicitly requeste } ``` -Navigator registers all eight logical tools unless the feature or an individual tool is disabled. Default names receive the `kompass_` prefix; configured aliases such as `start_workflow_session` are registered exactly. +The V1 adapter registers all nine logical tools unless the feature or an individual tool is disabled. Default names receive the `kompass_` prefix; configured aliases such as `start_workflow_session` are registered exactly. Navigator is scoped to the current project. New sessions inherit the calling session's agent, model, and variant unless explicitly overridden. Session IDs are ownership-checked, directories must be native managed worktrees, and lifecycle calls cannot target the calling session. `session_send` can switch the target session's agent or model before admitting a steered prompt. Waits default to `maxWaitMs`, cap requested timeouts at `maxWaitMs`, and use `timeoutMs: 0` for an immediate snapshot. Removal rejects the checkout, unmanaged worktrees, and worktrees containing active sessions. Navigator does not force removal, merge branches, create pull requests, or maintain a separate job database. -On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter for OpenCode's workspace API using its bundled `rift-snapshot` dependency. Navigator automatically uses Rift for `new_worktree` sessions when no `startCommand` is requested, falling back to Git worktrees otherwise. +On OpenCode 1, when experimental workspace adapters are available, Kompass registers a `rift` workspace adapter using its bundled `rift-snapshot` dependency. `worktree_create` uses Rift when no `startCommand` is requested, using Git worktrees otherwise. Pass the returned directory to `session_create`. ## Agent model diff --git a/packages/web/src/content/docs/docs/config/schema.mdx b/packages/web/src/content/docs/docs/config/schema.mdx index 56eb9d3..59425fe 100644 --- a/packages/web/src/content/docs/docs/config/schema.mdx +++ b/packages/web/src/content/docs/docs/config/schema.mdx @@ -54,7 +54,9 @@ Deprecated array fields still exist in the schema for compatibility, but the new `adapters.opencode.navigator` supports: - `enabled` -- `maxConcurrentSessions` from 1 through 8 +- `maxConcurrentSessions` from 1 through 8, enforced by the OpenCode 1 adapter only - positive `maxReadChars` - positive `maxOutputCharsPerItem`, no greater than `maxReadChars` - positive `maxWaitMs` + +OpenCode 2 registers eight native Navigator tools. `session_list` and the concurrency limit require session discovery APIs that V2 plugins do not expose. V2 worktree removal uses native force semantics without an active-session check. All other Navigator settings and supported tool aliases/disables apply to V2. diff --git a/packages/web/src/content/docs/docs/reference/tools/index.mdx b/packages/web/src/content/docs/docs/reference/tools/index.mdx index 0d5bcf2..69abdf2 100644 --- a/packages/web/src/content/docs/docs/reference/tools/index.mdx +++ b/packages/web/src/content/docs/docs/reference/tools/index.mdx @@ -14,6 +14,7 @@ Current built-in tools: - `ticket_load` - `ticket_sync` - `worktree_list` (OpenCode Navigator) +- `worktree_create` (OpenCode Navigator) - `session_create` (OpenCode Navigator) - `session_list` (OpenCode Navigator) - `session_read` (OpenCode Navigator) @@ -50,7 +51,7 @@ Creates or updates GitHub issues, renders checklist sections, and can append iss ## OpenCode Navigator tools -Navigator tools are enabled by default through the OpenCode 1 plugin API. These tools have not been ported to the V2 plugin API; OpenCode 2 uses Kompass's [native Rift worktree strategy](/docs/adapters/opencode/#opencode-2-worktrees). Use them only for explicitly requested native-session or managed-worktree workflows, not for subagent delegation; OpenCode's built-in `task` tool owns subagent lifecycle and waiting. Their default runtime names use the `kompass_` prefix. See [Kompass Navigator](./navigator/) for arguments and safety behavior. +Navigator is enabled by default on both OpenCode versions. V1 exposes all nine tools below. V2 exposes eight native tools, omitting `session_list` because the plugin API lacks session discovery. Use Navigator for explicitly requested native-session workflows; ordinary delegation uses OpenCode's built-in subagent mechanism. Default runtime names use the `kompass_` prefix. See [Kompass Navigator](./navigator/) for version-specific arguments and limits. ### `worktree_list` @@ -58,23 +59,25 @@ Lists the current checkout explicitly and all native OpenCode-managed worktrees. ### `session_create` -Creates a native session in the checkout, an existing managed workspace, or a newly created workspace; inherits the calling session's agent, model, and variant unless overridden, admits its initial prompt, and returns immediately. New workspaces prefer the registered Rift adapter when available and fall back to Git worktrees. +On V2, the initial `prompt` is optional. Omit it to create an idle session, then start execution with `session_send`. + +Both adapters create a native session in an optional `directory`, defaulting to the calling session's current location. An optional prompt starts execution immediately. Create new worktrees separately with `worktree_create`. The legacy session protocol selects agent/model only when prompting; pass overrides to `session_send` when starting an idle session. ### `session_list` -Lists paginated sessions filtered to the current OpenCode project, with location, agent, model, timestamps, and active or idle state. +V1 only. Lists paginated sessions filtered to the current OpenCode project, with location, agent, model, timestamps, and active or idle state. ### `session_read` -Reads a bounded page of recent projected messages. Tool outputs are excluded by default, and per-item and total truncation metadata is returned. +Reads bounded recent messages with optional tool output. V2 reads current model context; V1 supports paginated history. Configured per-item and total message limits apply. ### `session_send` -Sends a steered follow-up through the same session protocol selected by OpenCode Desktop, optionally switching the target agent or model first. +Admits a follow-up, optionally switching agent/model first. V2 uses native `steer` or `queue` delivery; V1 sends through the protocol selected by Desktop. ### `session_wait` -Polls the active-session API for the detected protocol until the first of up to eight sessions becomes idle, then returns bounded recent output. The timeout defaults to configured `maxWaitMs`, is capped by `maxWaitMs`, and `0` returns an immediate snapshot. +Waits for the first of up to eight sessions to become idle and returns bounded context. V2 uses native Effect waits and releases subscriptions on timeout or cancellation; V1 polls active state. Timeouts default to and are capped by `maxWaitMs`. V2 requires a positive timeout; V1 also supports `0` for an immediate snapshot. ### `session_interrupt` @@ -82,4 +85,8 @@ Interrupts active native execution for another session in the current project. ### `worktree_remove` -Removes an idle native managed worktree. It rejects the main checkout, unmanaged paths, and worktrees containing active sessions; force removal is not available. +Removes a native managed worktree. V2 uses native removal with optional `force`, protects the checkout and calling location, and cannot discover other active sessions. V1 checks for active sessions and has no force option. + +### `worktree_create` + +Creates a managed worktree without a session. V2 accepts `name`, `strategy`, and `branch`. V1 accepts `name` and `startCommand`. On either version, pass the returned directory to `session_create`. diff --git a/packages/web/src/content/docs/docs/reference/tools/navigator.mdx b/packages/web/src/content/docs/docs/reference/tools/navigator.mdx index 32e87dd..5cec92c 100644 --- a/packages/web/src/content/docs/docs/reference/tools/navigator.mdx +++ b/packages/web/src/content/docs/docs/reference/tools/navigator.mdx @@ -3,28 +3,72 @@ title: Kompass Navigator description: Native OpenCode session and managed-worktree orchestration tools. --- -Navigator is enabled by default through the OpenCode 1 plugin API. These eight tools have not been ported to the V2 plugin API; OpenCode 2 uses Kompass's [native Rift worktree strategy](/docs/adapters/opencode/#opencode-2-worktrees). Use Navigator only when the user explicitly requests native-session, managed-worktree, or multi-session workflow orchestration. Do not use it for subagent delegation: OpenCode's built-in `task` tool creates child sessions, waits for foreground tasks, and reports background completion automatically. Default runtime names use the `kompass_` prefix; set `adapters.opencode.navigator.enabled` to `false` to disable all eight tools. - -| Logical tool | Purpose | -| --- | --- | -| `worktree_list` | List the checkout and native managed worktrees. | -| `session_create` | Create a native session in the checkout, an existing managed workspace, or a newly created workspace, inheriting the caller's agent, model, and variant unless overridden, and admit its first prompt asynchronously. New workspaces prefer Rift when available. | -| `session_list` | List paginated sessions filtered to the current project. | -| `session_read` | Read a bounded page of recent messages with optional tool outputs. | -| `session_send` | Send a steered follow-up through the detected session API, optionally switching agent or model first. | -| `session_wait` | Poll the detected active-session API until the first of one to eight sessions becomes idle. The timeout defaults to and is capped by `maxWaitMs`; zero returns an immediate snapshot. | -| `session_interrupt` | Interrupt another session's active execution. | -| `worktree_remove` | Remove an idle native managed worktree without force. | +Navigator is enabled by default on both OpenCode versions. V1 exposes nine tools; V2 exposes eight tools through native session and worktree APIs, using the [Rift strategy](/docs/adapters/opencode/#opencode-2-worktrees) for default worktree creation. Use Navigator for explicitly requested native-session and multi-session workflows. Ordinary delegation uses OpenCode's built-in subagent mechanism. Default runtime names use the `kompass_` prefix; `adapters.opencode.navigator.enabled: false` disables Navigator. Individual logical names can be disabled or aliased through `tools`. + +| Logical tool | Versions | Purpose | +| --- | --- | --- | +| `worktree_list` | V1, V2 | List the checkout and native managed worktrees. | +| `worktree_create` | V1, V2 | Create a managed worktree without a session. | +| `session_create` | V1, V2 | Create a native session, inherit agent/model/variant unless overridden, admit its first prompt, and return immediately. | +| `session_list` | V1 | List paginated current-project sessions. | +| `session_read` | V1, V2 | Read bounded messages. V2 reads current model context; V1 supports paginated history. | +| `session_send` | V1, V2 | Send a follow-up, optionally switching agent/model. V2 supports steer or queue delivery. | +| `session_wait` | V1, V2 | Wait for the first of up to eight targets to become idle. V2 uses native waits with cancellation; V1 polls active-session state. | +| `session_interrupt` | V1, V2 | Interrupt another session's active execution. | +| `worktree_remove` | V1, V2 | Remove a managed worktree. V2 supports native force semantics; V1 checks active sessions and has no force option. | + +## Independent worktree lifecycle + +On V2, `worktree_create({ name?, strategy?, branch? })` returns `{ status: "created", worktree: { directory } }`. It uses the calling session's location as source. Omit `strategy` for the registered default, normally Rift. A starting `branch` requires `strategy: "git"`. Pass the returned directory to `session_create` in a separate call. + +Use `worktree_remove({ directory, force? })` to remove a managed directory. `force` defaults to false and permits discarding uncommitted files when true. Rift rejects snapshots with children even with force. The checkout and worktrees containing the calling session or plugin cannot be removed. + +V2 cannot discover all active sessions. Finish or interrupt known sessions using the directory before removal. The tool does not delete or interrupt sessions. V1 retains its active-session checks and has no force option. V1 standalone creation accepts `{ name?, startCommand? }`. + +## V2 create and follow up + +First call `kompass_worktree_create` with `{ "name": "parser-fix" }`. Then invoke `kompass_session_create` with the returned directory: + +```json +{ + "prompt": "Implement the parser fix and run tests.", + "title": "Parser fix", + "directory": "/projects/.rifts/repo/parser-fix" +} +``` + +V2 `session_create` accepts `directory?`, `prompt?`, `title?`, `agent?`, and `model?`. Without a directory, it uses the calling session's current location, even inside a worktree or subdirectory. An explicit directory can select the project checkout or a managed worktree. Relative paths resolve from the caller's directory. The removed `environment` field is rejected. V2 uses OpenCode's project setup rather than V1's `startCommand` argument. + +Session creation never creates or removes worktrees. Both adapters accept optional `directory` and `prompt` and have removed the `environment` argument. V1's legacy session protocol cannot persist agent/model selection for an idle session; provide those overrides to `session_send` with its first prompt. V1 returns `sessionID` and `directory` at the top level; V2 returns them inside `session`. + +Optional `agent` is a registered agent ID. V2's optional `model` uses `{ "providerID": "...", "id": "...", "variant": "..." }`; V1 uses `modelID` instead of `id`. + +With a prompt, `status` is `started`, `admitted` is `true`, and `session.sessionID` and `session.directory` identify the native session. Omit `prompt` on V2 to create an idle session with `status: "created"` and `admitted: false`. Start it later with `session_send`. Creation does not wait for an answer. + +Use the returned ID with `kompass_session_send`: + +```json +{ "sessionID": "ses_...", "prompt": "Also add regression coverage.", "delivery": "queue" } +``` + +`delivery` defaults to `steer`. Send targets a session ID and uses that session's stored location; it does not take a directory or move the session. Agent/model overrides are validated before switching. A later failure may leave a successfully applied agent/model change in place. + +Read with `session_read({ sessionID, turnLimit?, includeOutputs?, maxOutputCharsPerItem? })`. V2 defaults to the latest 20 current-context messages, excludes tool outputs, and caps requested output limits at configured limits. It does not expose a history cursor. + +Wait with `session_wait({ targets: [{ sessionID }], timeoutMs? })`. V2 requires a positive timeout, defaulting to and capped by `maxWaitMs`. `completed` means a target became idle; inspect `completed.session.outcome` for success, failure, or interruption. A timeout returns the target IDs without claiming they are all still active. Timeout, cancellation, and a winning target release the other native wait subscriptions without stopping their sessions. V1 also supports `timeoutMs: 0` for an immediate active-state snapshot. + +## V2 API limits + +The plugin context has no session list or active-session discovery API. V2 therefore omits `session_list`, cannot check other active sessions during removal, and does not enforce `maxConcurrentSessions`. Use known session IDs from tool results or Desktop. The feature toggle, aliases, `maxReadChars`, `maxOutputCharsPerItem`, and `maxWaitMs` apply on both versions. ## Safety and limits - Every session ID is validated against the current OpenCode project. - Existing-worktree paths must appear in OpenCode's native worktree list. - Send, wait, and interrupt reject the calling session. -- Reads default to ten recent messages, exclude tool outputs, and enforce configured per-item and total character limits. -- Navigator uses the same V1-or-V2 protocol choice as OpenCode Desktop so created sessions remain visible there. -- V2 create and send operations reject unknown agent names before admitting a prompt. -- Worktree removal rejects the main checkout, unmanaged worktrees, and worktrees containing active sessions. -- Partial creation errors identify resources already created and never automatically delete them. +- V1 uses Desktop's protocol detection; V2 calls the native plugin context directly. +- V2 create/send validate agent and model overrides at the destination location. +- V1 worktree removal rejects the main checkout, unmanaged worktrees, and worktrees containing active sessions. +- V2 partial creation failures return `status: "partial_failure"`, a failure phase, known session/worktree details, and recovery guidance. Progress updates report created resources before prompting. No automatic deletion occurs. -Navigator does not keep a separate job database. Existing native sessions and worktrees remain discoverable after plugin restarts. +Navigator does not keep a separate job database. V2 worktrees are discoverable after reloads and known native session IDs remain usable. Desktop's current New worktree button explicitly selects Git; the V2 Navigator create tool can use Rift without that UI change. diff --git a/packages/web/src/pages/index.astro b/packages/web/src/pages/index.astro index 3250888..93ff333 100644 --- a/packages/web/src/pages/index.astro +++ b/packages/web/src/pages/index.astro @@ -12,7 +12,7 @@ const heroHighlights = [ }, { eyebrow: "Adapters", - title: "OpenCode workflows and native session navigation" + title: "OpenCode workflows, independent worktrees and sessions" }, { eyebrow: "Integrations", From 27804fc191a048c573a80fa2ab3cc31aa08f099d Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Wed, 9 Sep 2026 17:18:41 -0300 Subject: [PATCH 7/7] fix: always execute OpenCode v2 commands inline --- README.md | 2 ++ packages/opencode/README.md | 2 ++ packages/opencode/test/v2-plugin.test.ts | 8 ++++---- packages/opencode/v2.ts | 15 +++++---------- .../src/content/docs/docs/adapters/opencode.mdx | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2aee792..bb62d11 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ The last active strategy registration wins. Unloading Kompass restores the previ ## OpenCode 2 session orchestration +All V2 commands execute inline in the invoking session, including commands marked `subtask` in shared definitions. They switch to the command's agent and submit its expanded template there. Separate sessions are created explicitly through Navigator tools. + Worktrees and sessions have separate lifecycles. `kompass_worktree_create({ name?, strategy?, branch? })` creates a worktree without a session. Pass its returned `directory` to `session_create`. Omit `directory` to use the calling session's current location, including when it is already in a worktree. Omit `prompt` to create an idle session, then start it with `session_send`. With a prompt, creation starts execution immediately. `session_send` targets a session ID and uses that session's location. `kompass_worktree_remove({ directory, force? })` removes a managed worktree through OpenCode. It rejects the checkout and any worktree containing the calling session or plugin. `force` defaults to false; true permits discarding uncommitted files. Rift still rejects snapshots with children. V2 cannot discover other active sessions, so finish or interrupt known sessions in that directory before removal. Removing a worktree does not delete its sessions. diff --git a/packages/opencode/README.md b/packages/opencode/README.md index 2aee792..bb62d11 100644 --- a/packages/opencode/README.md +++ b/packages/opencode/README.md @@ -82,6 +82,8 @@ The last active strategy registration wins. Unloading Kompass restores the previ ## OpenCode 2 session orchestration +All V2 commands execute inline in the invoking session, including commands marked `subtask` in shared definitions. They switch to the command's agent and submit its expanded template there. Separate sessions are created explicitly through Navigator tools. + Worktrees and sessions have separate lifecycles. `kompass_worktree_create({ name?, strategy?, branch? })` creates a worktree without a session. Pass its returned `directory` to `session_create`. Omit `directory` to use the calling session's current location, including when it is already in a worktree. Omit `prompt` to create an idle session, then start it with `session_send`. With a prompt, creation starts execution immediately. `session_send` targets a session ID and uses that session's location. `kompass_worktree_remove({ directory, force? })` removes a managed worktree through OpenCode. It rejects the checkout and any worktree containing the calling session or plugin. `force` defaults to false; true permits discarding uncommitted files. Rift still rejects snapshots with children. V2 cannot discover other active sessions, so finish or interrupt known sessions in that directory before removal. Removing a worktree does not delete its sessions. diff --git a/packages/opencode/test/v2-plugin.test.ts b/packages/opencode/test/v2-plugin.test.ts index 3caf82a..830b659 100644 --- a/packages/opencode/test/v2-plugin.test.ts +++ b/packages/opencode/test/v2-plugin.test.ts @@ -80,7 +80,7 @@ describe("OpenCode v2 plugin", () => { assert.deepEqual(f.disposed.sort(), ["agent", "command", "tool", "worktree"]); }); - test("native command effects preserve arguments, attachments, delivery, and session selection", async () => { + test("native commands always run inline and preserve arguments, attachments, and delivery", async () => { const f = fixture(); const resolved = await loadResolvedCommands(process.cwd()); const text = "Keep literal $& $$ $1 $ARGUMENTS and !`printf should-not-run`"; @@ -93,9 +93,9 @@ describe("OpenCode v2 plugin", () => { assert.equal(f.prompts[0]?.text, resolved.dev!.template.replaceAll("$ARGUMENTS", () => text)); assert.equal(f.prompts[0]?.files, files); assert.equal(f.prompts[0]?.delivery, "queue"); - assert.equal(f.prompts[0]?.sessionID, process.env.CI ? sessionID : "ses_child"); - assert.equal(f.createdSessions.length, process.env.CI ? 0 : 1); - if (!process.env.CI) assert.equal(f.createdSessions[0]?.agent, "worker"); + assert.equal(f.prompts[0]?.sessionID, sessionID); + assert.equal(f.createdSessions.length, 0); + assert.equal(f.switchedAgents.at(-1), "worker"); yield* f.commands.get("learn")!.execute({ sessionID, prompt: { text: "remember this" }, delivery: "steer" }); assert.equal(f.switchedAgents.at(-1), "worker"); assert.equal(f.prompts[1]?.sessionID, sessionID); diff --git a/packages/opencode/v2.ts b/packages/opencode/v2.ts index 9e7f41a..acee239 100644 --- a/packages/opencode/v2.ts +++ b/packages/opencode/v2.ts @@ -1,4 +1,4 @@ -import { Agent, Location, Plugin } from "@opencode/plugin/effect"; +import { Agent, Plugin } from "@opencode/plugin/effect"; import { Tool } from "@opencode/schema/tool"; import { Effect } from "effect"; import { z } from "zod"; @@ -69,19 +69,14 @@ function applyCommands( name, description: definition.description, execute: Effect.fn(`kompass.command.${name}`)(function* ({ sessionID, prompt, delivery }) { - let targetSessionID = sessionID; - if (definition.subtask) { - const session = yield* ctx.session.create({ - agent: definition.agent ? Agent.ID.make(definition.agent) : undefined, - location: Location.Ref.make({ directory: ctx.location.directory, workspaceID: ctx.location.workspaceID }), - }); - targetSessionID = session.id; - } else if (definition.agent) { + // V2 commands always execute in the invoking session, regardless of + // the shared definition's V1 subtask setting. + if (definition.agent) { yield* ctx.session.switchAgent({ sessionID, agent: Agent.ID.make(definition.agent) }); } yield* ctx.session.prompt({ ...prompt, - sessionID: targetSessionID, + sessionID, text: definition.template.includes("$ARGUMENTS") ? definition.template.replaceAll("$ARGUMENTS", () => prompt.text) : `${definition.template}\n\n${prompt.text}`.trim(), diff --git a/packages/web/src/content/docs/docs/adapters/opencode.mdx b/packages/web/src/content/docs/docs/adapters/opencode.mdx index a3dc2bb..ca80f3e 100644 --- a/packages/web/src/content/docs/docs/adapters/opencode.mdx +++ b/packages/web/src/content/docs/docs/adapters/opencode.mdx @@ -116,7 +116,7 @@ Most command execution runs on the bundled `worker` agent. Kompass also provides the `worker`, `planner`, and `reviewer` agent roles for workflow execution, planning, and review-specific execution. -Commands run as subtasks outside CI unless their definition opts out. The `/inline` variants of `/branch`, `/commit`, `/commit-and-push`, `/pr/create`, and `/ship`, plus `/learn`, opt out so they can use the invoking session directly. +On V2, all commands run inline in the invoking session, regardless of the shared `subtask` setting. The adapter switches to the command's agent and submits its expanded template in that session. On V1, commands run as subtasks outside CI unless their definition opts out. The `/inline` variants of `/branch`, `/commit`, `/commit-and-push`, `/pr/create`, and `/ship`, plus `/learn`, opt out on V1. ## Useful OpenCode notes