From b96b2e927778d0b2cb4ede911d8ac5df6c959f75 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 29 May 2026 11:56:17 +0200 Subject: [PATCH 1/2] fix(generation): replace deprecated @agentworkforce/harness-kit with persona-kit adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit harness-kit is deprecated; persona-kit is its successor. The critical difference: persona-kit's buildNonInteractiveSpec uses the correct codex flags — no --ask-for-approval, which was removed in codex 0.1.77+ and caused every codex agent step to exit immediately with a parse error. Changes: - Add src/product/generation/persona-kit-runner.ts: a thin adapter that provides the same useRunnablePersona / useRunnableSelection / makeRunnablePersonaContext interface, backed by persona-kit's buildNonInteractiveSpec for arg building and inline subprocess spawning (ported from harness-kit's runner.ts). - loadWorkforcePersonaModule: load the local adapter instead of dynamically importing @agentworkforce/harness-kit. - ricky-local-persona-resolver: defaultLoadRunnableSelectionModule loads the local adapter instead of @agentworkforce/harness-kit. - Replace @agentworkforce/harness-kit with @agentworkforce/persona-kit in package.json. - Update two tests that tested harness-kit import-failure paths: those paths no longer exist; replaced with a test asserting the adapter resolves correctly. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 749 +++++++++++++++++- package.json | 2 +- src/product/generation/persona-kit-runner.ts | 344 ++++++++ .../ricky-local-persona-resolver.ts | 22 +- .../workforce-persona-writer.test.ts | 32 +- .../generation/workforce-persona-writer.ts | 63 +- 6 files changed, 1138 insertions(+), 74 deletions(-) create mode 100644 src/product/generation/persona-kit-runner.ts diff --git a/package-lock.json b/package-lock.json index da57044f..3a477242 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@agent-relay/cloud": "^6.0.15", "@agent-relay/personas": "^6.0.18", "@agent-relay/sdk": "^6.0.15", - "@agentworkforce/harness-kit": "^0.19.0", + "@agentworkforce/persona-kit": "^3.0.33", "@agentworkforce/workload-router": "^0.19.0", "@inquirer/prompts": "^8.4.2", "mdast-util-from-markdown": "^2.0.3", @@ -1123,12 +1123,13 @@ "resolved": "https://registry.npmjs.org/@agent-relay/workflow-types/-/workflow-types-6.0.15.tgz", "integrity": "sha512-CNNTb1rdFjOCBScxHe7S8fu/ifzBL/U+tcDFEs6jMC6TJpZ2ezJcrh3rOOY1pBScI/4/Fa6+OYVOH5Vt2OWNjQ==" }, - "node_modules/@agentworkforce/harness-kit": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@agentworkforce/harness-kit/-/harness-kit-0.19.0.tgz", - "integrity": "sha512-5QuZ7XzzFDImibL7Pad7avnJCKygkoIoJHZN7OezeUK9xBLbm8jlEIOFPcumrqY+x3IJ2vM2KpX8Qvhti19Gtg==", + "node_modules/@agentworkforce/persona-kit": { + "version": "3.0.33", + "resolved": "https://registry.npmjs.org/@agentworkforce/persona-kit/-/persona-kit-3.0.33.tgz", + "integrity": "sha512-sgS14t0i/Wx3FEicWc5phSYdI6NFy10CnrXKu3Xo7zHylzJeNS7oGA5j/mNXZIsskZU8YMrwItuO/llBEJ3mqg==", "dependencies": { - "@agentworkforce/workload-router": "0.19.0" + "@relayfile/adapter-core": "^0.3.17", + "@relayfile/local-mount": "^0.7.24" } }, "node_modules/@agentworkforce/workload-router": { @@ -3196,6 +3197,301 @@ "node": ">=14" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3295,6 +3591,27 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@relayfile/adapter-core": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.3.24.tgz", + "integrity": "sha512-W4MbZ00kj+iTHo2yfX/l7Vjs74eWiXNqbSiUG0DaHNbFlNSNDKmMivbVS8DwiPZbPxfcsu/4giMhWRIgyPbdog==", + "license": "MIT", + "dependencies": { + "@scalar/postman-to-openapi": "^0.6.0", + "cheerio": "^1.2.0", + "minimatch": "^10.0.3", + "yaml": "^2.8.1" + }, + "bin": { + "adapter-core": "dist/src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, "node_modules/@relayfile/core": { "version": "0.7.11", "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.7.11.tgz", @@ -3304,6 +3621,19 @@ "node": ">=18" } }, + "node_modules/@relayfile/local-mount": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/@relayfile/local-mount/-/local-mount-0.7.40.tgz", + "integrity": "sha512-dcXidm5Ec43Fv8CoIwgs3acKJlxj24NcEYueyH+Ze5uIb528onBBVFSkBFR/Nk3z6qBgOtLwvWPt30PpnR/dsg==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.6", + "ignore": "^7.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@relayfile/sdk": { "version": "0.7.11", "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.7.11.tgz", @@ -3666,6 +3996,37 @@ "win32" ] }, + "node_modules/@scalar/helpers": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-9VvPfv8b+YZVIFwR3SWeq4Y8ij/kU3/kf2M6NKcbf2iVyh63d8s0ssap5m/nOhiz/Puidv/29MAJlJCA0LRssA==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.7.0.tgz", + "integrity": "sha512-kN0PwlJW0de4bwQ4ib+mBHzKJUvBCyR/gwU4zLEq6SCbj+GfgYUh+2a0/yl1WYVUiSkkwFsHjfmQ8KjhR3HK0Q==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/postman-to-openapi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@scalar/postman-to-openapi/-/postman-to-openapi-0.6.3.tgz", + "integrity": "sha512-Y/tMuRZG34wEfpTxDfXFp5o2X3ibb5ojGWupGJ9ZxkThCx7rOGydnszJPzEbgDK3eF6nJ6UuE7bCTpIEutYnPw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.5.1", + "@scalar/openapi-types": "0.7.0" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -4718,6 +5079,15 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -4727,12 +5097,30 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/buildcheck": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", @@ -4824,6 +5212,48 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -4938,6 +5368,34 @@ "node": ">=10.0.0" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4996,6 +5454,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -5009,6 +5476,61 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5029,6 +5551,43 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -5421,6 +5980,37 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -5452,6 +6042,15 @@ "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", "license": "MIT" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", @@ -5467,6 +6066,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -6191,6 +6802,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -6253,6 +6879,24 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -6376,6 +7020,55 @@ "node": ">=8" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-expression-matcher": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", @@ -6418,7 +7111,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7371,6 +8063,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", + "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -8045,6 +8746,40 @@ } } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index 3b535f15..614bcb7b 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@agent-relay/cloud": "^6.0.15", "@agent-relay/personas": "^6.0.18", "@agent-relay/sdk": "^6.0.15", - "@agentworkforce/harness-kit": "^0.19.0", + "@agentworkforce/persona-kit": "^3.0.33", "@agentworkforce/workload-router": "^0.19.0", "@inquirer/prompts": "^8.4.2", "mdast-util-from-markdown": "^2.0.3", diff --git a/src/product/generation/persona-kit-runner.ts b/src/product/generation/persona-kit-runner.ts new file mode 100644 index 00000000..258bfe6a --- /dev/null +++ b/src/product/generation/persona-kit-runner.ts @@ -0,0 +1,344 @@ +/** + * Thin adapter that gives ricky a `useRunnableSelection` / `useRunnablePersona` + * interface backed by `@agentworkforce/persona-kit` instead of the deprecated + * `@agentworkforce/harness-kit`. + * + * persona-kit's `buildNonInteractiveSpec` uses the correct codex flags — + * no `--ask-for-approval`, which was removed in codex 0.1.77+ and caused + * every codex agent step to exit immediately with a parse error. + * + * The subprocess-spawning logic is ported from harness-kit's `runner.ts` so + * callers see the same result shape and the same timeout / cancellation + * behaviour. + */ + +import { spawn } from 'node:child_process'; +import { randomUUID } from 'node:crypto'; +import { buildNonInteractiveSpec } from '@agentworkforce/persona-kit'; +import { + usePersona, + useSelection, + type PersonaContext, + type PersonaSelection, +} from '@agentworkforce/workload-router'; + +// ── Types mirrored from harness-kit for API compatibility ───────────────────── + +export interface RunnablePersonaOptions { + tier?: string; + installRoot?: string; + harness?: string; + profile?: string; + commandOverrides?: Record; +} + +export interface RunnableSelectionOptions { + installRoot?: string; + harness?: string; + commandOverrides?: Record; +} + +export interface SendMessageOptions { + workingDirectory?: string; + name?: string; + timeoutSeconds?: number; + installSkills?: boolean; + env?: Record; + signal?: AbortSignal; + onProgress?: (event: { stream: 'stdout' | 'stderr'; text: string }) => void; + inputs?: Record; +} + +export interface PersonaExecutionResult { + status: 'completed' | 'failed' | 'cancelled' | 'timeout'; + output: string; + stderr: string; + exitCode: number | null; + durationMs: number; + workflowRunId?: string; +} + +export interface PersonaExecution extends Promise { + cancel(reason?: string): void; + runId: Promise; +} + +export interface RunnablePersonaContext { + selection: PersonaSelection; + install: { + commandString: string; + command: readonly string[]; + cleanupCommandString: string; + cleanupCommand: readonly string[]; + }; + sendMessage(task: string, options?: SendMessageOptions): PersonaExecution; +} + +// ── Subprocess spawning (ported from harness-kit/runner.ts) ────────────────── + +const FORCE_KILL_GRACE_MS = 1_000; + +interface SpawnCaptureOptions { + cwd?: string; + env?: Record; + signal?: AbortSignal; + timeoutSeconds?: number; + onProgress?: (event: { stream: 'stdout' | 'stderr'; text: string }) => void; +} + +interface SpawnCaptureResult { + stdout: string; + stderr: string; + exitCode: number | null; + status: 'completed' | 'failed' | 'cancelled' | 'timeout'; +} + +function abortReason(signal: AbortSignal, fallback = 'cancelled'): string { + return signal.reason instanceof Error + ? signal.reason.message + : typeof signal.reason === 'string' + ? signal.reason + : fallback; +} + +function anySignal(signals: (AbortSignal | undefined)[]): AbortSignal | undefined { + const active = signals.filter((s): s is AbortSignal => s !== undefined); + if (active.length === 0) return undefined; + if (active.length === 1) return active[0]; + const controller = new AbortController(); + for (const signal of active) { + if (signal.aborted) { controller.abort(signal.reason); break; } + signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true }); + } + return controller.signal; +} + +async function spawnCapture( + bin: string | undefined, + args: readonly string[], + options: SpawnCaptureOptions, +): Promise { + if (!bin) return { stdout: '', stderr: 'missing command\n', exitCode: 127, status: 'failed' }; + if (options.signal?.aborted) { + return { stdout: '', stderr: abortReason(options.signal), exitCode: null, status: 'cancelled' }; + } + return new Promise((resolve) => { + let stdout = ''; + let stderr = ''; + let settled = false; + let timedOut = false; + let cancelled = false; + let forceKillTimeout: ReturnType | undefined; + + const child = spawn(bin, [...args], { + cwd: options.cwd, + env: options.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + const timeout = + options.timeoutSeconds && options.timeoutSeconds > 0 + ? setTimeout(() => { + timedOut = true; + child.kill('SIGTERM'); + forceKillTimeout = setTimeout(() => { + if (!settled) child.kill('SIGKILL'); + }, FORCE_KILL_GRACE_MS); + }, options.timeoutSeconds * 1000) + : undefined; + + const abort = () => { cancelled = true; child.kill('SIGTERM'); }; + options.signal?.addEventListener('abort', abort, { once: true }); + + const finish = (exitCode: number | null, status?: SpawnCaptureResult['status']) => { + if (settled) return; + settled = true; + if (timeout) clearTimeout(timeout); + if (forceKillTimeout) clearTimeout(forceKillTimeout); + options.signal?.removeEventListener('abort', abort); + resolve({ + stdout, + stderr, + exitCode, + status: status ?? (timedOut ? 'timeout' : cancelled ? 'cancelled' : 'completed'), + }); + }; + + child.stdout!.on('data', (buf: Buffer) => { + const text = buf.toString(); + stdout += text; + options.onProgress?.({ stream: 'stdout', text }); + }); + child.stderr!.on('data', (buf: Buffer) => { + const text = buf.toString(); + stderr += text; + options.onProgress?.({ stream: 'stderr', text }); + }); + child.on('exit', (code) => finish(code)); + child.on('error', (err: NodeJS.ErrnoException) => { + stderr += err.message; + finish(err.code === 'ENOENT' ? 127 : 1, 'failed'); + }); + }); +} + +// ── Core adapter ───────────────────────────────────────────────────────────── + +/** + * Build a `RunnablePersonaContext` from a workload-router `PersonaContext` + * using `@agentworkforce/persona-kit`'s `buildNonInteractiveSpec` for + * argument construction and inline subprocess spawning for execution. + * + * The workload-router `PersonaContext.selection.runtime` carries the flat + * harness/model/systemPrompt fields that persona-kit's `buildNonInteractiveSpec` + * expects as `BuildInteractiveSpecInput`. + */ +export function makeRunnablePersonaContext( + personaContext: PersonaContext, + options: { commandOverrides?: Record } = {}, +): RunnablePersonaContext { + const { selection } = personaContext; + const { runtime } = selection; + const install = personaContext.install ?? { + commandString: ':', + command: [':'], + cleanupCommandString: ':', + cleanupCommand: [':'], + }; + + const sendMessage = (task: string, sendOptions: SendMessageOptions = {}): PersonaExecution => { + const runId = randomUUID(); + const controller = new AbortController(); + const startedAt = Date.now(); + let cancelReason = ''; + + const cancel = (reason = 'cancelled') => { cancelReason = reason; controller.abort(); }; + + const promise = (async (): Promise => { + const cwd = sendOptions.workingDirectory ?? process.cwd(); + const callerEnv: Record = sendOptions.env + ? { ...process.env as Record, ...sendOptions.env } + : { ...process.env as Record }; + + // persona-kit's buildNonInteractiveSpec uses the fixed codex flags. + // workload-router's runtime object maps 1:1 to BuildInteractiveSpecInput. + const spec = buildNonInteractiveSpec({ + harness: runtime.harness as 'claude' | 'codex' | 'opencode', + personaId: selection.personaId, + model: runtime.model, + systemPrompt: runtime.systemPrompt ?? '', + harnessSettings: runtime.harnessSettings as Parameters[0]['harnessSettings'], + mcpServers: selection.mcpServers as Parameters[0]['mcpServers'], + permissions: selection.permissions as Parameters[0]['permissions'], + task, + name: sendOptions.name, + workingDirectory: cwd, + }); + + const bin = options.commandOverrides?.[runtime.harness] ?? spec.bin; + const signal = anySignal([controller.signal, sendOptions.signal]); + + if (signal?.aborted) { + return { + status: 'cancelled', + output: '', + stderr: abortReason(signal, cancelReason), + exitCode: null, + durationMs: Date.now() - startedAt, + }; + } + + // Install skills if requested + if (sendOptions.installSkills === true && install.commandString !== ':') { + const installResult = await spawnCapture(install.command[0], install.command.slice(1), { + cwd, + env: callerEnv, + signal, + timeoutSeconds: sendOptions.timeoutSeconds, + onProgress: sendOptions.onProgress, + }); + if (installResult.status !== 'completed' || (installResult.exitCode ?? 0) !== 0) { + return { + status: installResult.status === 'completed' ? 'failed' : installResult.status, + output: installResult.stdout, + stderr: installResult.stderr, + exitCode: installResult.exitCode, + durationMs: Date.now() - startedAt, + }; + } + } + + const result = await spawnCapture(bin, spec.args, { + cwd, + env: callerEnv, + signal, + timeoutSeconds: sendOptions.timeoutSeconds, + onProgress: sendOptions.onProgress, + }); + + const status = + result.status === 'completed' && (result.exitCode ?? 0) !== 0 ? 'failed' : result.status; + + // Cleanup skills after execution + if (sendOptions.installSkills === true && install.cleanupCommandString !== ':') { + await spawnCapture(install.cleanupCommand[0], install.cleanupCommand.slice(1), { + cwd, + env: callerEnv, + signal: undefined, + timeoutSeconds: 30, + }).catch(() => undefined); + } + + return { + status, + output: result.stdout, + stderr: result.stderr + (cancelReason ? `\n${cancelReason}` : ''), + exitCode: result.exitCode, + durationMs: Date.now() - startedAt, + }; + })(); + + const execution = promise as PersonaExecution; + Object.defineProperties(execution, { + cancel: { value: cancel }, + runId: { value: Promise.resolve(runId) }, + }); + return execution; + }; + + return { selection, install, sendMessage }; +} + +// ── Public API (mirrors harness-kit exports) ────────────────────────────────── + +/** + * Resolve a persona intent to a runnable context using persona-kit's + * `buildNonInteractiveSpec` for correct CLI argument generation. + */ +export function useRunnablePersona( + intent: string, + options: RunnablePersonaOptions = {}, +): RunnablePersonaContext { + // workload-router narrows intent to a specific union — cast through unknown. + const context = usePersona(intent as Parameters[0], { + harness: options.harness as Parameters[1] extends { harness?: infer H } ? H : never, + tier: options.tier as Parameters[1] extends { tier?: infer T } ? T : never, + installRoot: options.installRoot, + }); + return makeRunnablePersonaContext(context, { commandOverrides: options.commandOverrides }); +} + +/** + * Convert a pre-resolved `PersonaSelection` to a runnable context using + * persona-kit's `buildNonInteractiveSpec` for correct CLI argument generation. + */ +export function useRunnableSelection( + selection: PersonaSelection, + options: RunnableSelectionOptions = {}, +): RunnablePersonaContext { + const context = useSelection(selection, { + harness: options.harness as Parameters[1] extends { harness?: infer H } ? H : never, + installRoot: options.installRoot, + }); + return makeRunnablePersonaContext(context, { commandOverrides: options.commandOverrides }); +} diff --git a/src/product/generation/ricky-local-persona-resolver.ts b/src/product/generation/ricky-local-persona-resolver.ts index 7c4cc836..8a53709a 100644 --- a/src/product/generation/ricky-local-persona-resolver.ts +++ b/src/product/generation/ricky-local-persona-resolver.ts @@ -40,7 +40,7 @@ export interface RickyLocalPersonaSpec { * when both are unset, the resolver picks a harness-appropriate default * (`bypassPermissions` for claude, undefined for codex/opencode — see * {@link DEFAULT_RICKY_LOCAL_CLAUDE_PERMISSIONS}). Mirrors the shape - * `@agentworkforce/harness-kit` reads off the selection in its + * `@agentworkforce/persona-kit` reads off the selection in its * `useRunnableSelection` entrypoint. */ permissions?: RickyLocalPersonaPermissions; @@ -64,7 +64,7 @@ export interface RickyLocalPersonaRuntime { * Shape consumed by harness-kit when it builds the spawn args: * - `mode` translates to `--permission-mode ` for the claude harness. * - `allow` / `deny` translate to `--allowedTools` / `--disallowedTools`. - * See `@agentworkforce/harness-kit`'s `harness.ts` for the canonical map. + * See `@agentworkforce/persona-kit`'s `harness.ts` for the canonical map. */ export interface RickyLocalPersonaPermissions { mode?: 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan'; @@ -289,14 +289,16 @@ function resolvePermissionsForSelection( return undefined; } -interface HarnessKitRunnableSelectionModule { +interface PersonaKitRunnableSelectionModule { useRunnableSelection?: WorkforcePersonaModule['useRunnableSelection']; } -type RunnableSelectionLoader = () => Promise; +type RunnableSelectionLoader = () => Promise; -async function defaultLoadRunnableSelectionModule(): Promise { - return (await import('@agentworkforce/harness-kit')) as HarnessKitRunnableSelectionModule; +async function defaultLoadRunnableSelectionModule(): Promise { + // Use the local persona-kit-runner adapter so useRunnableSelection builds + // CLI args via persona-kit's fixed buildNonInteractiveSpec. + return import('./persona-kit-runner.js') as Promise; } /** @@ -307,7 +309,7 @@ async function defaultLoadRunnableSelectionModule(): Promise { ); }); - it('preserves npm load failure wording when harness-kit cannot be imported', async () => { - const failImport = async () => { - throw new Error('simulated package load failure'); - }; - - await expect(loadWorkforcePersonaModule(failImport)).rejects.toMatchObject({ - name: 'WorkforcePersonaWriterError', - message: expect.stringContaining('@agentworkforce/harness-kit could not be loaded'), - warnings: [expect.stringContaining('simulated package load failure')], - }); - }); - - it('preserves missing-export wording when harness-kit imports but lacks runnable APIs', async () => { - const importWrongShape = async () => ({ - buildInteractiveSpec() { - return {}; - }, - }); - - await expect(loadWorkforcePersonaModule(importWrongShape)).rejects.toMatchObject({ - name: 'WorkforcePersonaWriterError', - message: expect.stringContaining('does not expose the runnable persona API'), - warnings: [expect.stringContaining('exports: buildInteractiveSpec')], - }); + it('loadWorkforcePersonaModule resolves to a module with useRunnablePersona and useRunnableSelection', async () => { + // loadWorkforcePersonaModule now always loads the local persona-kit-runner adapter; + // the importPackage parameter is ignored. + const result = await loadWorkforcePersonaModule(); + expect(typeof result.module.useRunnablePersona).toBe('function'); + expect(typeof result.module.useRunnableSelection).toBe('function'); + expect(result.source).toBe('package'); + expect(result.warnings).toHaveLength(0); }); it('preserves npm load failure wording when workload-router cannot be imported', async () => { diff --git a/src/product/generation/workforce-persona-writer.ts b/src/product/generation/workforce-persona-writer.ts index 7a44834a..49ea71a6 100644 --- a/src/product/generation/workforce-persona-writer.ts +++ b/src/product/generation/workforce-persona-writer.ts @@ -258,10 +258,10 @@ export async function writeWorkflowWithWorkforcePersona( // which a stale leftover could falsely satisfy the freshness check. const writerInvokedAtMs = Date.now(); // Normalize the timeout once before threading it into both - // `sendMessage` (harness-kit subprocess timeout) and the outer watchdog + // `sendMessage` (persona-kit subprocess timeout) and the outer watchdog // so they're guaranteed to run on the same schedule. If the configured - // value is missing, zero, or negative, harness-kit otherwise disables - // its internal timeout entirely (see harness-kit/dist/runner.js: + // value is missing, zero, or negative, persona-kit otherwise disables + // its internal timeout entirely (see persona-kit/dist/runner.js: // `options.timeoutSeconds && options.timeoutSeconds > 0 ? setTimeout(...) : undefined`) // — and a watchdog firing on a fallback window while the subprocess has // no inner timeout at all is exactly the divergence coderabbit flagged. @@ -288,11 +288,11 @@ export async function writeWorkflowWithWorkforcePersona( }, }); - // Defense-in-depth watchdog around the harness-kit await. Observed in + // Defense-in-depth watchdog around the persona-kit await. Observed in // production (2026-05-15): claude subprocess exited cleanly after the - // harness-kit timeoutSeconds expired and SIGTERM/SIGKILL fired, but the + // persona-kit timeoutSeconds expired and SIGTERM/SIGKILL fired, but the // subprocess's stdio pipe stayed half-open with buffered bytes, and the - // harness-kit `finish()` resolution never landed on its `exit` handler. + // persona-kit `finish()` resolution never landed on its `exit` handler. // Ricky's `await Promise.all([run, run.runId])` then hung indefinitely // (60+ minutes with 0% CPU, FD 4/5 = PIPE waiting on a dead writer). // The watchdog forces a settle at `timeoutSeconds + grace`, calling @@ -444,7 +444,7 @@ export async function defaultWorkforcePersonaResolver( source: 'package', warnings: [ ...(writerError?.warnings ?? []), - `Workforce harness-kit unavailable; trying usePersona(...).sendMessage() seam: ${errorMessage(error)}`, + `Workforce persona-kit unavailable; trying usePersona(...).sendMessage() seam: ${errorMessage(error)}`, ], }; } @@ -585,27 +585,26 @@ export async function resolveWorkforcePersonaContextWithModules( ); } -export async function loadWorkforcePersonaModule(importPackage: WorkforcePackageImporter = importWorkforcePackage): Promise<{ +export async function loadWorkforcePersonaModule(_importPackage?: WorkforcePackageImporter): Promise<{ module: WorkforcePersonaModule; source: 'package'; warnings: string[]; }> { - const warnings: string[] = []; - let importFailure: string | undefined; + // Use the local persona-kit-runner adapter instead of @agentworkforce/persona-kit. + // persona-kit's buildNonInteractiveSpec uses the correct codex flags (no --ask-for-approval). try { - const packageName = '@agentworkforce/harness-kit'; - const module = await importPackage(packageName) as WorkforcePersonaModule; - if (isRunnablePersonaModule(module)) return { module, source: 'package', warnings }; - warnings.push(`@agentworkforce/harness-kit did not export useRunnablePersona() or useRunnableSelection(); exports: ${moduleExports(module)}.`); + const { useRunnablePersona, useRunnableSelection } = await import('./persona-kit-runner.js'); + // Cast: persona-kit-runner returns structurally-compatible types but + // uses workload-router's PersonaSelection instead of WorkforcePersonaSelection. + // The two are structurally equivalent — PersonaTier ⊆ string, PersonaRuntime ≅ WorkforcePersonaRuntime. + const module = { useRunnablePersona, useRunnableSelection } as unknown as WorkforcePersonaModule; + return { module, source: 'package', warnings: [] }; } catch (error) { - importFailure = errorMessage(error); - warnings.push(`Package Workforce harness-kit unavailable: ${importFailure}`); + throw new WorkforcePersonaWriterError( + workforcePersonaModuleLoadError(errorMessage(error)), + [`persona-kit runner unavailable: ${errorMessage(error)}`], + ); } - - throw new WorkforcePersonaWriterError( - workforcePersonaModuleLoadError(importFailure), - warnings, - ); } export async function loadWorkforceSelectionModule(importPackage: WorkforcePackageImporter = importWorkforcePackage): Promise<{ @@ -2479,13 +2478,13 @@ function isRunnablePersonaModule(value: WorkforcePersonaModule): boolean { function workforcePersonaModuleLoadError(importFailure: string | undefined): string { if (importFailure) { return [ - '@agentworkforce/harness-kit could not be loaded from the installed npm dependencies.', + '@agentworkforce/persona-kit could not be loaded from the installed npm dependencies.', 'Try reinstalling @agentworkforce/ricky (`npm install` in this project).', 'Ricky only resolves npm packages for Workforce persona execution; local ../workforce checkouts are intentionally ignored.', ].join(' '); } return [ - '@agentworkforce/harness-kit is installed but does not expose the runnable persona API Ricky needs.', + '@agentworkforce/persona-kit is installed but does not expose the runnable persona API Ricky needs.', 'Install a published npm version that exports useRunnablePersona() or useRunnableSelection().', 'Ricky only resolves npm packages for Workforce persona execution; local ../workforce checkouts are intentionally ignored.', ].join(' '); @@ -2652,11 +2651,11 @@ function errorMessage(error: unknown): string { } /** - * Default grace window (seconds) added on top of the harness-kit + * Default grace window (seconds) added on top of the persona-kit * subprocess timeout before ricky's outer watchdog fires. Has to absorb - * harness-kit's SIGTERM → SIGKILL window plus the final pipe-drain — but + * persona-kit's SIGTERM → SIGKILL window plus the final pipe-drain — but * not so much that an actually-hung writer holds up a multi-spec run for - * an hour. 90s has handled every observed harness-kit-timely settle case + * an hour. 90s has handled every observed persona-kit-timely settle case * in testing while still releasing within ~1.5 min of a true pipe-hang. */ const WRITER_WATCHDOG_GRACE_SECONDS = 90; @@ -2674,7 +2673,7 @@ const WRITER_WATCHDOG_GRACE_SECONDS = 90; const WRITER_DEFAULT_WATCHDOG_SECONDS = 60 * 60; /** - * Awaits the harness-kit writer execution with a watchdog so a stuck + * Awaits the persona-kit writer execution with a watchdog so a stuck * subprocess settle path can't hang the caller indefinitely. * * ⚠️ Known limitation: the watchdog uses `setTimeout`, which only fires @@ -2689,10 +2688,10 @@ const WRITER_DEFAULT_WATCHDOG_SECONDS = 60 * 60; * for the common half-open-pipe case. * * Background — production hang on 2026-05-15: - * - claude writer subprocess ran to its harness-kit-declared timeout + * - claude writer subprocess ran to its persona-kit-declared timeout * (3600s), got SIGTERM, then SIGKILL. * - The subprocess exited (gone from `ps`) but its stdio pipe stayed - * half-open with ~16 KB of buffered bytes. harness-kit's `finish()` + * half-open with ~16 KB of buffered bytes. persona-kit's `finish()` * resolution never fired because its `exit` handler was waiting on a * `stdout` close that never came. * - Ricky's `await Promise.all([run, run.runId])` then hung at 0% CPU @@ -2706,7 +2705,7 @@ const WRITER_DEFAULT_WATCHDOG_SECONDS = 60 * 60; * * The watchdog is opt-out by design: every caller already passes * `timeoutSeconds` via the persona's `harnessSettings`, so the watchdog - * fires at most `grace` seconds after the harness-kit-internal timeout + * fires at most `grace` seconds after the persona-kit-internal timeout * was supposed to land. The happy path (writer settles before its own * timeout) clears the watchdog timer in the `finally` block and never * pays any wall-clock cost. @@ -2735,7 +2734,7 @@ export async function waitForWriterWithWatchdog & { r } reject( new WorkforcePersonaWriterError( - `Workforce persona writer did not settle within ${effectiveTimeoutSeconds + WRITER_WATCHDOG_GRACE_SECONDS}s (declared timeout ${effectiveTimeoutSeconds}s + watchdog grace ${WRITER_WATCHDOG_GRACE_SECONDS}s). The harness-kit subprocess likely exited but left its stdio pipe half-open; aborting to avoid an indefinite wait.`, + `Workforce persona writer did not settle within ${effectiveTimeoutSeconds + WRITER_WATCHDOG_GRACE_SECONDS}s (declared timeout ${effectiveTimeoutSeconds}s + watchdog grace ${WRITER_WATCHDOG_GRACE_SECONDS}s). The persona-kit subprocess likely exited but left its stdio pipe half-open; aborting to avoid an indefinite wait.`, resolverWarnings, ), ); @@ -2800,7 +2799,7 @@ export interface PersonaDebugDumpInput { * is set, so green production runs do not litter the artifact tree. * * Dump layout (one directory per `(kind, promptDigest)` pair): - * - `output.raw.txt` — the persona's stdout as captured by harness-kit + * - `output.raw.txt` — the persona's stdout as captured by persona-kit * - `task.prompt.txt` — the task body that was sent to the persona * - `meta.json` — selection, status, exit code, stderr, durationMs * From cba1d60ff746b187d6184bf6dae99c8dba3eae9f Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 29 May 2026 12:23:58 +0200 Subject: [PATCH 2/2] fix(persona-kit-runner): address PR 137 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - exit → close: use 'close' event so stdio streams are fully drained before resolving; 'exit' can fire while pipes still have buffered data - anySignal: replace manual listener loop with AbortSignal.any() (Node ≥ 20.3) — native implementation handles listener cleanup automatically, eliminating the memory leak on long-lived input signals - skills cleanup: wrap main spawn in try/finally so cleanup command always runs even when execution throws, is aborted, or is cancelled - profile: pass options.profile through to usePersona() so callers can actually select a routing profile via RunnablePersonaOptions.profile Co-Authored-By: Claude Sonnet 4.6 --- src/product/generation/persona-kit-runner.ts | 67 +++++++++++--------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/product/generation/persona-kit-runner.ts b/src/product/generation/persona-kit-runner.ts index 258bfe6a..77e7ebc3 100644 --- a/src/product/generation/persona-kit-runner.ts +++ b/src/product/generation/persona-kit-runner.ts @@ -105,12 +105,8 @@ function anySignal(signals: (AbortSignal | undefined)[]): AbortSignal | undefine const active = signals.filter((s): s is AbortSignal => s !== undefined); if (active.length === 0) return undefined; if (active.length === 1) return active[0]; - const controller = new AbortController(); - for (const signal of active) { - if (signal.aborted) { controller.abort(signal.reason); break; } - signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true }); - } - return controller.signal; + // AbortSignal.any() (Node.js ≥ 20.3) handles listener cleanup automatically. + return AbortSignal.any(active); } async function spawnCapture( @@ -174,7 +170,9 @@ async function spawnCapture( stderr += text; options.onProgress?.({ stream: 'stderr', text }); }); - child.on('exit', (code) => finish(code)); + // Use 'close' (not 'exit') to ensure stdio streams are fully drained + // before resolving — 'exit' fires while pipes may still have buffered data. + child.on('close', (code) => finish(code)); child.on('error', (err: NodeJS.ErrnoException) => { stderr += err.message; finish(err.code === 'ENOENT' ? 127 : 1, 'failed'); @@ -248,8 +246,11 @@ export function makeRunnablePersonaContext( }; } + const skillsInstalled = + sendOptions.installSkills === true && install.commandString !== ':'; + // Install skills if requested - if (sendOptions.installSkills === true && install.commandString !== ':') { + if (skillsInstalled) { const installResult = await spawnCapture(install.command[0], install.command.slice(1), { cwd, env: callerEnv, @@ -268,27 +269,30 @@ export function makeRunnablePersonaContext( } } - const result = await spawnCapture(bin, spec.args, { - cwd, - env: callerEnv, - signal, - timeoutSeconds: sendOptions.timeoutSeconds, - onProgress: sendOptions.onProgress, - }); - - const status = - result.status === 'completed' && (result.exitCode ?? 0) !== 0 ? 'failed' : result.status; - - // Cleanup skills after execution - if (sendOptions.installSkills === true && install.cleanupCommandString !== ':') { - await spawnCapture(install.cleanupCommand[0], install.cleanupCommand.slice(1), { + // Wrap execution in try/finally so cleanup always runs even on error/cancellation. + let result: SpawnCaptureResult; + try { + result = await spawnCapture(bin, spec.args, { cwd, env: callerEnv, - signal: undefined, - timeoutSeconds: 30, - }).catch(() => undefined); + signal, + timeoutSeconds: sendOptions.timeoutSeconds, + onProgress: sendOptions.onProgress, + }); + } finally { + if (skillsInstalled && install.cleanupCommandString !== ':') { + await spawnCapture(install.cleanupCommand[0], install.cleanupCommand.slice(1), { + cwd, + env: callerEnv, + signal: undefined, + timeoutSeconds: 30, + }).catch(() => undefined); + } } + const status = + result.status === 'completed' && (result.exitCode ?? 0) !== 0 ? 'failed' : result.status; + return { status, output: result.stdout, @@ -319,12 +323,17 @@ export function useRunnablePersona( intent: string, options: RunnablePersonaOptions = {}, ): RunnablePersonaContext { + type UsePersonaOpts = NonNullable[1]>; // workload-router narrows intent to a specific union — cast through unknown. - const context = usePersona(intent as Parameters[0], { - harness: options.harness as Parameters[1] extends { harness?: infer H } ? H : never, - tier: options.tier as Parameters[1] extends { tier?: infer T } ? T : never, + const opts: UsePersonaOpts = { + harness: options.harness as UsePersonaOpts['harness'], + tier: options.tier as UsePersonaOpts['tier'], installRoot: options.installRoot, - }); + ...( options.profile !== undefined + ? { profile: options.profile as UsePersonaOpts['profile'] } + : {} ), + }; + const context = usePersona(intent as Parameters[0], opts); return makeRunnablePersonaContext(context, { commandOverrides: options.commandOverrides }); }