From 3df469abecbae501c9011527ac48d62527f106af Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sat, 27 Jun 2026 20:21:50 +0200 Subject: [PATCH 1/3] feat(factory): dispatch relayflows from integration events --- package-lock.json | 1076 ++++++++--------- package.json | 10 +- ...y-cloud-watches-local-node-linear-issue.md | 29 +- ...-unified-node-architecture-linear-issue.md | 4 +- ...near-issue-factory-phase-3-fleet-client.md | 4 +- ...issue-factory-phase-4-node-registration.md | 6 +- src/cli/fleet.test.ts | 6 + src/cli/fleet.ts | 32 + src/dispatch/relayflow-registry.test.ts | 273 +++++ src/dispatch/relayflow-registry.ts | 221 ++++ src/index.ts | 19 + src/node/factory-node.test.ts | 54 +- src/node/factory-node.ts | 105 +- src/orchestrator/factory.test.ts | 65 + src/orchestrator/factory.ts | 34 + src/ports/fleet.ts | 2 +- .../__tests__/event-client.test.ts | 40 + src/subscriptions/event-client.ts | 2 + src/types.ts | 7 + 19 files changed, 1353 insertions(+), 636 deletions(-) create mode 100644 src/dispatch/relayflow-registry.test.ts create mode 100644 src/dispatch/relayflow-registry.ts diff --git a/package-lock.json b/package-lock.json index a2b11fc..21cdea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { "name": "@agent-relay/factory", - "version": "0.1.5", + "version": "0.1.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-relay/factory", - "version": "0.1.5", + "version": "0.1.11", "license": "Apache-2.0", "dependencies": { "@agent-relay/cloud": "^9.0.2", "@agent-relay/fleet": "^9.0.2", "@agent-relay/harness-driver": "^9.0.2", "@agent-relay/integration-prompts": "^9.0.2", + "@relayfile/relay-helpers": "^0.4.2", "@relayfile/sdk": "^0.10.9", + "@relayflows/core": "^1.0.3", "agent-relay": "^9.0.1", "zod": "^3.25.76" }, @@ -21,8 +23,6 @@ "factory": "bin/factory.mjs" }, "devDependencies": { - "@relayflows/cli": "^1.0.3", - "@relayflows/core": "^1.0.3", "@types/node": "^22.10.2", "esbuild": "^0.24.2", "tsc-alias": "^1.8.17", @@ -30,7 +30,7 @@ "vitest": "^4.1.8" }, "engines": { - "node": ">=20" + "node": ">=20.18.1" } }, "node_modules/@agent-relay/broker-darwin-arm64": { @@ -1498,24 +1498,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/sunos-x64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", @@ -1784,6 +1766,85 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@relayfile/adapter-core": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.4.4.tgz", + "integrity": "sha512-4Z5IYrRJqf9QA0OczM1wxWAuRL8n66a+s2/GBdgjolM0IlgQJzShB92Z1O6/RWz7er2eDC4kVnfv0BRKxcVibA==", + "license": "Apache-2.0", + "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/adapter-core/node_modules/@scalar/helpers": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.2.18.tgz", + "integrity": "sha512-w1d4tpNEVZ293oB2BAgLrS0kVPUtG3eByNmOCJA5eK9vcT4D3cmsGtWjUaaqit0BQCsBFHK51rasGvSWnApYTw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@relayfile/adapter-core/node_modules/@scalar/openapi-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.5.4.tgz", + "integrity": "sha512-2pEbhprh8lLGDfUI6mNm9EV104pjb3+aJsXrFaqfgOSre7r6NlgM5HcSbsLjzDAnTikjJhJ3IMal1Rz8WVwiOw==", + "license": "MIT", + "dependencies": { + "zod": "^4.3.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@relayfile/adapter-core/node_modules/@scalar/postman-to-openapi": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@scalar/postman-to-openapi/-/postman-to-openapi-0.4.10.tgz", + "integrity": "sha512-s7TECz1DLSXggRYEEVjoBXLxY3nKCU9n4zA7FxRywxsG485qmr2gMHqU5plFJDC59RUxcIOo+V+LbOpKo1EQUQ==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.2.18", + "@scalar/openapi-types": "0.5.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@relayfile/adapter-core/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@relayfile/adapter-linear": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-linear/-/adapter-linear-0.4.2.tgz", + "integrity": "sha512-Yg0hv/RqEB5WJ4RA4LX9JinCxxT/01GSdd+0HeT0L9P36EX27kEEuzWmNwWEpjDHQMcYykEocruEJHuV4XXGCQ==", + "license": "Apache-2.0", + "dependencies": { + "@relayfile/adapter-core": "^0.4.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, "node_modules/@relayfile/core": { "version": "0.10.9", "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.10.9.tgz", @@ -1845,6 +1906,16 @@ "linux" ] }, + "node_modules/@relayfile/relay-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@relayfile/relay-helpers/-/relay-helpers-0.4.2.tgz", + "integrity": "sha512-/SnZW5vEtNBJkYslLOuw8JIMU1edYmY9AXEJF52k4H3Q1Z/I3GPAShXCbQmCry07Q42CM/+1SEk1mLzDaU5cVw==", + "license": "Apache-2.0", + "dependencies": { + "@relayfile/adapter-core": "^0.4.3", + "@relayfile/adapter-linear": "^0.4.2" + } + }, "node_modules/@relayfile/sdk": { "version": "0.10.9", "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.10.9.tgz", @@ -2135,6 +2206,69 @@ "@relayfile/mount-linux-x64": "0.8.30" } }, + "node_modules/@relayflows/core/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@relayflows/core/node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@relayflows/core/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@relayflows/core/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@relayflows/github-primitive": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@relayflows/github-primitive/-/github-primitive-1.0.3.tgz", @@ -3534,6 +3668,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", @@ -3594,12 +3737,30 @@ "url": "https://opencollective.com/express" } }, + "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/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -3686,6 +3847,48 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3769,6 +3972,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3888,6 +4097,34 @@ "node": ">= 8" } }, + "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", @@ -3946,6 +4183,61 @@ "node": ">=8" } }, + "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/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -3993,6 +4285,43 @@ "node": ">= 0.8" } }, + "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", @@ -4636,6 +4965,37 @@ "node": ">=16.9.0" } }, + "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/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -5100,22 +5460,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/listr2": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", - "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.2.0", - "eventemitter3": "^5.0.4", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^10.0.0" - }, - "engines": { - "node": ">=22.13.0" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -5307,6 +5651,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", @@ -5393,12 +5752,24 @@ "node": ">=0.10.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { "node": ">=0.10.0" } }, @@ -5520,6 +5891,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/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6497,6 +6917,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -6611,456 +7040,6 @@ } } }, - "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", @@ -7088,50 +7067,6 @@ } } }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, "node_modules/vitest/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -7225,6 +7160,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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7257,35 +7226,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index cb06ba2..d6beef8 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "type": "module", "engines": { - "node": ">=20" + "node": ">=20.18.1" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -59,13 +59,17 @@ "@agent-relay/fleet": "^9.0.2", "@agent-relay/harness-driver": "^9.0.2", "@agent-relay/integration-prompts": "^9.0.2", + "@relayfile/relay-helpers": "^0.4.2", "@relayfile/sdk": "^0.10.9", + "@relayflows/core": "^1.0.3", "agent-relay": "^9.0.1", "zod": "^3.25.76" }, + "overrides": { + "@scalar/postman-to-openapi": "0.4.10", + "listr2": "9.0.5" + }, "devDependencies": { - "@relayflows/cli": "^1.0.3", - "@relayflows/core": "^1.0.3", "@types/node": "^22.10.2", "esbuild": "^0.24.2", "tsc-alias": "^1.8.17", diff --git a/planning/factory-cloud-watches-local-node-linear-issue.md b/planning/factory-cloud-watches-local-node-linear-issue.md index af50b2c..0e9b612 100644 --- a/planning/factory-cloud-watches-local-node-linear-issue.md +++ b/planning/factory-cloud-watches-local-node-linear-issue.md @@ -1,4 +1,31 @@ -Title: [factory] EPIC — cloud watches → local-node execution; label-driven single agent / workflow / team +# ⚠️ HISTORICAL — v1 epic, superseded 2026-06-18 + +This document is the **original (v1) factory extraction epic** written 2026-06-15. It captures the initial framing — a local-vs-cloud execution split, with the factory brain on the operator's laptop and a "minimal slice" of fleet placement. + +**That framing was superseded mid-extraction** by Will Washburn's `relay/specs/fleet-delivery.md` RFC (2026-06-06) and the conversation it triggered. The unified-node model that shipped is meaningfully different from what's described below: + +- **There is no local-vs-cloud execution split.** Daytona sandboxes, laptops, mac minis, EC2 boxes are all the same primitive: nodes. Each advertises capabilities; placement picks an eligible node. +- **The factory is a spec-emitter**, not an executor. It emits `spawn { capability, persona, … }` invocations into the relay fleet; placement is the fleet's job. +- **`single | workflow | team` are recipes** — patterns over one spawn primitive — not three distinct code paths. + +**Current architecture (read these, not this doc):** + +- `relay/specs/fleet-delivery.md` — the load-bearing RFC. Two planes (messaging fabric + compute layer), spawn-as-action, idempotency + at-least-once + reconcile. +- `cloud/packages/web/lib/proactive-runtime/factory-cloud-orchestrator.ts` — the cloud-hosted factory brain (Phase 2 / the v1 doc's "cloud lift", now real). +- `cloud/packages/web/lib/proactive-runtime/factory-fleet-emitter.ts` — emits fleet spawns from the orchestrator. +- `cloud/packages/web/lib/proactive-runtime/team-launch-n1.ts` — proactive runtime, **explicitly cut over from the legacy Daytona `launchMember` path to fleet** (see the cutover comment around line 503). +- `factory/` repo (`AgentWorkforce/factory`, published as `@agent-relay/factory` on npm) — the extracted package. +- `factory/src/fleet/relay-fleet-client.ts` — `RelayFleetClient`, the thin client over the fleet protocol. + +**Phases that actually shipped under the unified model:** P1 StateStore port (pear#371), P2 config split (pear#372), P3 publish-prep (pear#370), P4 extraction (factory `5f32a5a` + npm `@agent-relay/factory@0.1.1`), P5 Pear teardown (pear#373), p7 recipe scoping (factory#1), p10 RelayFleetClient (factory#2), p13 node-definition (factory#3), plus the proactive cutover (cloud `factory-cloud-orchestrator.ts` + `factory-fleet-emitter.ts` + `team-launch-n1.ts`). + +**Why this doc is preserved:** §4 (package extraction details), §5 (config split), and §8 (phased plan structure) were correct in v1 and were used as-is during execution. §2 (architecture diagram), §3 (label → shape), §6 ("minimal slice" of #1056), and §10 (non-goals) are wrong under the shipped architecture — they describe a local-cloud split that does not exist in the code. + +A proper v2 rewrite was planned (see the v2 handoff drafted at `pear/handoff-factory-unified-node-architecture.md`, Deliverable 1) but was never produced because the unified-node architecture shipped directly as code without an intermediate v2 document. This stamp serves in its place. + +--- + +Title: [factory] EPIC — cloud watches → local-node execution; label-driven single agent / workflow / team (HISTORICAL — see stamp above) Team: AR Suggested status: Design / Epic diff --git a/planning/factory-unified-node-architecture-linear-issue.md b/planning/factory-unified-node-architecture-linear-issue.md index a639b30..71d9956 100644 --- a/planning/factory-unified-node-architecture-linear-issue.md +++ b/planning/factory-unified-node-architecture-linear-issue.md @@ -82,7 +82,7 @@ The recipe is one knob with three expansions — not three execution paths. The | Recipe (label) | Spawn-set emitted | Capability | Persona / workflow source | Roster from repo labels | |---|---|---|---|---| | `agent:single` | **1** spawn `{ capability: 'spawn:claude' (or per-persona harness), persona: , node?: , session_ref? }` | `spawn:claude` / `spawn:codex` | `agents//persona.ts` | ignored for count (always 1); repo label still informs placement (which checkout/node) | -| `agent:workflow` | **1** spawn `{ capability: 'workflow:run', workflow: '.{yaml,ts,py}', inputs }` — the node runs `relayflows run `, which may emit further **child spawns** | `workflow:run` | workflow file defines its own roster; personas referenced by its steps resolve from `agents/` | ignored for roster; repo labels become workflow inputs | +| `agent:workflow` | **1** spawn `{ capability: 'workflow:run', workflow: '.{yaml,ts,py}', inputs }` — the node invokes the Relayflows SDK in-process, which may emit further **child spawns** | `workflow:run` | workflow file defines its own roster; personas referenced by its steps resolve from `agents/` | ignored for roster; repo labels become workflow inputs | | `agent:team` | **N** implementer spawns + **1** reviewer spawn + roster metadata. This is the logic that today lives in `cloud/.../teams/spawn-team.ts`, reconstructed as a recipe over the spawn primitive | `spawn:claude` / `spawn:codex` per member | `agents/cloud-team-implementer/persona.ts`, `agents/cloud-team-reviewer/persona.ts` | **one implementer per repo label** (capped at 4 per AR-272); reviewer naming unchanged | Concrete example (today's AR-267 team): labels `cloud`, `relayfile`, `agent:team` → emit `spawn{spawn:claude, persona: cloud-team-implementer, node-target via cloud checkout}`, `spawn{… relayfile checkout}`, and `spawn{spawn:claude, persona: cloud-team-reviewer}`. Placement, execution, and completion are all fleet-side. @@ -155,7 +155,7 @@ v1 called relay#1056 "a minimal slice the factory needs." Under unified-node, ** ## 8. Open questions (surface to operator) -1. **`workflow:run` capability handler shape.** When a node picks up `{capability:'workflow:run'}`, does it (a) shell out to the `relayflows` CLI, (b) embed the runtime in-process, or (c) call relayflows as a service? Proposed in Phase 3: shell out to `relayflows run ` on the node (simplest; the node already has the harness + repo checkout; child spawns ride the same fleet). Confirm. +1. **`workflow:run` capability handler shape.** Decided: the node embeds the Relayflows runtime through `@relayflows/core`; it does not depend on a globally installed CLI. The node already has the harness + repo checkout, and child spawns ride the same fleet. 2. **Single-recipe in cloud.** Cloud has no single-agent path today (only team via `spawn-team.ts`, proactive via `team-launch-n1`). `agent:single` is just a 1-spawn recipe — confirm it needs nothing beyond team-recipe at N=1. 3. **Multi-node placement preference.** Laptop + mac-mini both advertise `spawn:claude` — RFC §6 says least-loaded. Good enough for v1? (Assumed yes.) 4. **Persona discovery single source.** Both cloud (team-recipe construction) and the node-side workflow runtime must read `AgentWorkforce/agents/`. Confirm both point at the same registry. diff --git a/planning/linear-issue-factory-phase-3-fleet-client.md b/planning/linear-issue-factory-phase-3-fleet-client.md index dc78747..31def33 100644 --- a/planning/linear-issue-factory-phase-3-fleet-client.md +++ b/planning/linear-issue-factory-phase-3-fleet-client.md @@ -28,7 +28,7 @@ Implement `RelayFleetClient implements FleetClient` (the port at `pear/packages/ - **`SpawnInput.capability`** is `'spawn:claude' | 'spawn:codex'` today (ports/fleet.ts:4–15); extend the type to include **`'workflow:run'`** for the workflow recipe. - **invocationId lifecycle (RFC §7):** the client supplies an `invocationId` (idempotency key); observes `pending → dispatched(node) → completed(agent_id)`; relies on the fleet for dedup, reschedule-on-node-loss, and reconcile (first-to-`completed` wins). The factory does NOT implement placement, scheduling, or reconcile — it observes the lifecycle and reports completion upward. -- **`workflow:run` capability handler (open question 1 — proposed):** the node-side handler for `{capability:'workflow:run', workflow:}` **shells out to `relayflows run `** in the node's repo checkout. Rationale: the node already has the harness + checkout; child spawns the workflow emits ride the same fleet; no embedded runtime or service dependency. The `relayflows` CLI is a dependency of the node's harness definition (Phase 4). Confirm with operator before building. +- **`workflow:run` capability handler:** the node-side handler for `{capability:'workflow:run', workflow:}` invokes `@relayflows/core` in the node's repo checkout. The node already has the harness + checkout; child spawns the workflow emits ride the same fleet. No globally installed Relayflows CLI is required. - **No reuse of `InternalFleetClient`'s broker-direct path** beyond reference — `RelayFleetClient` talks the fleet protocol, not the local `HarnessDriverClient`. ## End-to-end verification (captured artifact required) @@ -46,7 +46,7 @@ Implement `RelayFleetClient implements FleetClient` (the port at `pear/packages/ 3. An `agent:single` spawn round-trips the fleet and lands on whichever eligible node is live (factory targeted none). 4. Completion observed via the `invocationId` lifecycle; Linear writeback fires. 5. Node-loss mid-spawn reschedules the same `invocationId` with no double-spawn (captured). -6. The `workflow:run` handler decision (shell-out to `relayflows run`) is documented + implemented or explicitly deferred with the chosen alternative recorded. +6. The `workflow:run` handler decision (embedded Relayflows SDK) is documented + implemented. ## Out of scope diff --git a/planning/linear-issue-factory-phase-4-node-registration.md b/planning/linear-issue-factory-phase-4-node-registration.md index 8dbb3dc..d28b49c 100644 --- a/planning/linear-issue-factory-phase-4-node-registration.md +++ b/planning/linear-issue-factory-phase-4-node-registration.md @@ -20,7 +20,7 @@ Today the operator runs `pear factory start` — a daemon that owns orchestratio - **Reads `NodeConfig`** (p2): `workspaceId`, `capabilities` (`spawn:claude` / `spawn:codex` / `workflow:run`), and repo checkout paths (`cloneRoot` / `clonePaths`, pear#369 compact form). - **Registers + advertises** per RFC §9 control surface: `node.register` (name, capabilities, version, max_agents, tags, resume cursor), `node.heartbeat` (~10–15s: load, active_agents), `node.deregister` on shutdown, `inventory.sync` (re-announce live agents on reconnect: agent_id, name, invocationId, session_ref). -- **Handles `action.invoke`** from Relaycast (RFC §9 Relaycast→Broker): for `spawn:claude`/`spawn:codex`, spawn the harness in the mapped checkout (the existing local PTY path — `InternalFleetClient`'s `spawnPty` is the reference impl); for `workflow:run`, **shell out to `relayflows run `** in the checkout (per Phase 3's contract). Emits `agent.register` / `action.result` / `delivery.ack` back. +- **Handles `action.invoke`** from Relaycast (RFC §9 Relaycast→Broker): for `spawn:claude`/`spawn:codex`, spawn the harness in the mapped checkout (the existing local PTY path — `InternalFleetClient`'s `spawnPty` is the reference impl); for `workflow:run`, invoke the Relayflows SDK in the checkout. Emits `agent.register` / `action.result` / `delivery.ack` back. - **No orchestration logic.** No triage, no merge-gate, no batch state — all cloud (Phase 2). The node is dumb compute that advertises what it can run. - **The broker already auto-starts:** `agent-relay fleet serve` calls `startBrokerWithPortFallback` (`relay/packages/cli/src/cli/commands/fleet.ts:144`) before serving — one command boots the broker + registers the node, no separate `agent-relay up`. @@ -31,7 +31,7 @@ A laptop, mac mini, EC2 box, or autospawned Daytona sandbox all run this same re 1. From a machine with NO running broker, run `agent-relay local factory` with only a `NodeConfig`. 2. Capture: the broker auto-starts, the node appears in the fleet roster (`agent-relay fleet nodes`) with the advertised capabilities + a live heartbeat. 3. From cloud factory triage (Phase 2), an `agent:single` spawn placed by Relaycast onto this node executes in the correct local checkout; capture the agent running + `action.result` completion. -4. Capture an `agent:workflow` spawn: the node runs `relayflows run ` and any child spawns ride the fleet. +4. Capture an `agent:workflow` spawn: the node invokes the Relayflows SDK and any child spawns ride the fleet. 5. Capture reconnect reconcile: drop the node's network < TTL, restore; `inventory.sync` re-announces live agents; no duplicate spawns. ## Acceptance criteria @@ -39,7 +39,7 @@ A laptop, mac mini, EC2 box, or autospawned Daytona sandbox all run this same re 1. `agent-relay local factory` registers the machine as a node from `NodeConfig` alone; broker auto-starts (cold-broker proof). 2. Node advertises `capabilities` + `clonePaths`; appears in the roster with heartbeat. 3. A cloud-placed `spawn:claude` / `spawn:codex` executes in the mapped checkout; `action.result` reports completion. -4. A cloud-placed `workflow:run` runs `relayflows run ` in the checkout. +4. A cloud-placed `workflow:run` invokes `@relayflows/core` in the checkout. 5. Reconnect inventory-sync reconciles without duplicate spawns (captured). 6. The command contains zero orchestration logic (no triage/merge/state). diff --git a/src/cli/fleet.test.ts b/src/cli/fleet.test.ts index 4910853..0795fce 100644 --- a/src/cli/fleet.test.ts +++ b/src/cli/fleet.test.ts @@ -584,6 +584,9 @@ describe('fleet CLI runtime', () => { expect(ensureLocalMount).toHaveBeenCalledWith('rw_7ccfea89', process.cwd(), { acceptableWorkspaceIds: ['50587328-441d-4acb-b8f3-dbe1b3c5de99'], }) + expect(ensureLocalMount).toHaveBeenCalledWith('rw_7ccfea89', '/work/pear', { + acceptableWorkspaceIds: ['50587328-441d-4acb-b8f3-dbe1b3c5de99'], + }) } finally { await rm(root, { recursive: true, force: true }) } @@ -669,6 +672,9 @@ describe('fleet CLI runtime', () => { expect(ensureLocalMount).toHaveBeenCalledWith('factory-cli-test', process.cwd(), { acceptableWorkspaceIds: undefined, }) + expect(ensureLocalMount).toHaveBeenCalledWith('factory-cli-test', '/work/pear', { + acceptableWorkspaceIds: undefined, + }) expect(createFactory).toHaveBeenCalledTimes(1) expect(factory.start).toHaveBeenCalledWith({ mode: 'live' }) expect(factory.runLoop).not.toHaveBeenCalled() diff --git a/src/cli/fleet.ts b/src/cli/fleet.ts index 0e2b77f..7541757 100644 --- a/src/cli/fleet.ts +++ b/src/cli/fleet.ts @@ -283,6 +283,7 @@ async function runFactoryCommand( await (deps.ensureLocalMount ?? ensureLocalMount)(workspaceId, process.cwd(), { acceptableWorkspaceIds: acceptableMountIds, }) + await ensureClonePathMounts(deps, workspaceId, config, acceptableMountIds) const waiter = createStopSignalWaiter() let stoppedBySignal = false const flushAndExit = async (code: number): Promise => { @@ -313,6 +314,7 @@ async function runFactoryCommand( } } if (command.action === 'run-once') { + await ensureClonePathMounts(deps, workspaceId, config, acceptableMountIds) writeJson(out, await factory.runOnce({ dryRun: globals.dryRun })) return 0 } @@ -344,6 +346,7 @@ async function runFactoryCommand( return 0 } + await ensureClonePathMounts(deps, workspaceId, config, acceptableMountIds) const removeSignalHandlers = installFactoryStopSignalHandlers(factory, { processLike: deps.stopSignalProcessLike, }) @@ -375,6 +378,35 @@ async function runFactoryCommand( return 0 } +/** + * Ensures the relayfile mount is running at each configured clone path so + * spawned agents can resolve `.integrations` relative to their working + * directory (the checkout path). The mount daemon started at the daemon CWD + * is not automatically accessible from a different directory, and agents need + * these paths for integration writebacks (Slack, GitHub, etc.). + */ +async function ensureClonePathMounts( + deps: FleetCliDeps, + workspaceId: string, + config: FactoryConfig, + acceptableMountIds?: readonly string[], +): Promise { + const mountFn = deps.ensureLocalMount ?? ensureLocalMount + const mountOpts = { acceptableWorkspaceIds: acceptableMountIds } + const daemonCwd = resolve(process.cwd()) + for (const clonePath of new Set(Object.values(config.clonePaths))) { + const resolved = resolve(clonePath) + if (resolved !== daemonCwd) { + try { + await mountFn(workspaceId, resolved, mountOpts) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + process.stderr.write(`[factory] warning: could not start relayfile mount at ${resolved}: ${message}\n`) + } + } + } +} + function parseFactoryCommand(args: string[]): ParsedCommand { const [action, issueOrPr, ...flags] = args if (action === 'start') { diff --git a/src/dispatch/relayflow-registry.test.ts b/src/dispatch/relayflow-registry.test.ts new file mode 100644 index 0000000..98f28c4 --- /dev/null +++ b/src/dispatch/relayflow-registry.test.ts @@ -0,0 +1,273 @@ +import { describe, expect, it, vi } from 'vitest' + +import { linearClient, githubClient } from '@relayfile/relay-helpers' + +import type { WorkflowRunnerInput, WorkflowRunnerResult } from '../node/factory-node' +import { + createRelayflowPolicyRegistry, + dispatchRelayflowForTrigger, + triggerEventFromChangeEvent, + RelayflowPolicyRegistry, + type RelayflowPolicyEntry, + type TriggerEvent, + type TriggerMapperContext, +} from './relayflow-registry' +import type { ChangeEvent } from '../ports' + +function makeRunner(status: WorkflowRunnerResult['status'] = 'completed', runId = 'run-001') { + const calls: WorkflowRunnerInput[] = [] + const runner = vi.fn(async (input: WorkflowRunnerInput): Promise => { + calls.push(input) + return { cwd: input.cwd, runner: '@relayflows/core', status, runId } + }) + return { runner, calls } +} + +describe('RelayflowPolicyRegistry', () => { + it('returns undefined for an unregistered trigger', () => { + const registry = createRelayflowPolicyRegistry() + expect(registry.resolve('linear.issue.created')).toBeUndefined() + expect(registry.size).toBe(0) + }) + + it('resolves a registered trigger to its policy entry', () => { + const entry: RelayflowPolicyEntry = { + templatePath: 'workflows/triage.yaml', + mapInputs: (event) => ({ issueId: event.resourceId }), + } + const registry = createRelayflowPolicyRegistry() + registry.register('linear.issue.created', entry) + + expect(registry.resolve('linear.issue.created')).toBe(entry) + expect(registry.size).toBe(1) + }) + + it('supports chained register calls and multiple triggers', () => { + const registry = createRelayflowPolicyRegistry() + registry + .register('linear.issue.created', { + templatePath: 'workflows/triage.yaml', + mapInputs: () => ({}), + }) + .register('github.pull_request.review_submitted', { + templatePath: 'workflows/review-gate.yaml', + mapInputs: () => ({}), + }) + + expect(registry.size).toBe(2) + expect([...registry.triggers()]).toEqual([ + 'linear.issue.created', + 'github.pull_request.review_submitted', + ]) + }) + + it('allows overriding an existing trigger entry', () => { + const registry = new RelayflowPolicyRegistry() + const first: RelayflowPolicyEntry = { templatePath: 'a.yaml', mapInputs: () => ({}) } + const second: RelayflowPolicyEntry = { templatePath: 'b.yaml', mapInputs: () => ({}) } + + registry.register('linear.issue.created', first) + registry.register('linear.issue.created', second) + + expect(registry.resolve('linear.issue.created')).toBe(second) + expect(registry.size).toBe(1) + }) +}) + +describe('dispatchRelayflowForTrigger', () => { + it('returns null when no policy is registered for the trigger', async () => { + const registry = createRelayflowPolicyRegistry() + const { runner } = makeRunner() + const result = await dispatchRelayflowForTrigger( + { trigger: 'linear.issue.created', resourceId: 'issue-uuid-1' }, + registry, + { cwd: '/work', workflowRunner: runner }, + ) + expect(result).toBeNull() + expect(runner).not.toHaveBeenCalled() + }) + + it('invokes the workflow runner with the correct template path and shaped inputs', async () => { + const { runner, calls } = makeRunner('completed', 'run-abc') + const registry = createRelayflowPolicyRegistry() + registry.register('linear.issue.created', { + templatePath: 'workflows/new-issue.yaml', + mapInputs: (event) => ({ + issueId: event.resourceId, + source: 'linear', + }), + }) + + const event: TriggerEvent = { trigger: 'linear.issue.created', resourceId: 'uuid-99' } + const result = await dispatchRelayflowForTrigger(event, registry, { + cwd: '/work/factory', + workflowRunner: runner, + }) + + expect(result).not.toBeNull() + expect(result!.trigger).toBe('linear.issue.created') + expect(result!.templatePath).toBe('workflows/new-issue.yaml') + expect(result!.runId).toBe('run-abc') + expect(result!.status).toBe('completed') + expect(result!.inputs).toEqual({ issueId: 'uuid-99', source: 'linear' }) + + expect(calls).toHaveLength(1) + expect(calls[0]).toMatchObject({ + workflow: 'workflows/new-issue.yaml', + cwd: '/work/factory', + inputs: { issueId: 'uuid-99', source: 'linear' }, + }) + }) + + it('supports async input mappers (e.g. reading provider state)', async () => { + const { runner, calls } = makeRunner() + const registry = createRelayflowPolicyRegistry() + + registry.register('github.pull_request.review_submitted', { + templatePath: 'workflows/review-gate.yaml', + mapInputs: async (event) => { + // Simulate an async provider read (e.g. relay-helpers client.getIssue) + await Promise.resolve() + return { prNumber: event.payload?.number, repo: event.payload?.repo } + }, + }) + + await dispatchRelayflowForTrigger( + { + trigger: 'github.pull_request.review_submitted', + payload: { number: 42, repo: 'AgentWorkforce/factory' }, + }, + registry, + { cwd: '/work', workflowRunner: runner }, + ) + + expect(calls[0]!.inputs).toEqual({ prNumber: 42, repo: 'AgentWorkforce/factory' }) + }) + + it('passes relay-helpers-backed provider clients to the mapper', async () => { + const { runner } = makeRunner() + const capturedCtx: { linear?: TriggerMapperContext['linear'] } = {} + + const registry = createRelayflowPolicyRegistry() + registry.register('linear.issue.created', { + templatePath: 'workflows/triage.yaml', + mapInputs: (_event, ctx) => { + capturedCtx.linear = ctx.linear + return {} + }, + }) + + await dispatchRelayflowForTrigger( + { trigger: 'linear.issue.created' }, + registry, + { cwd: '/work', mountRoot: '/mnt/.integrations', workflowRunner: runner }, + ) + + // The context must expose a relay-helpers LinearClient (not a hand-rolled object) + // by checking that the issues path matches the writeback-path catalog resolution. + const expected = linearClient({ mountRoot: '/mnt/.integrations' }) + expect(capturedCtx.linear!.issues.path()).toBe(expected.issues.path()) + // Catalog-backed: should be /linear/issues, not any hardcoded variation + expect(capturedCtx.linear!.issues.path()).toBe('/linear/issues') + }) + + it('uses catalog-backed paths for GitHub via relay-helpers (not hardcoded)', async () => { + const { runner } = makeRunner() + const capturedCtx: { github?: TriggerMapperContext['github'] } = {} + + const registry = createRelayflowPolicyRegistry() + registry.register('github.pull_request.review_submitted', { + templatePath: 'workflows/review-gate.yaml', + mapInputs: (_event, ctx) => { + capturedCtx.github = ctx.github + return {} + }, + }) + + await dispatchRelayflowForTrigger( + { trigger: 'github.pull_request.review_submitted' }, + registry, + { cwd: '/work', workflowRunner: runner }, + ) + + // Verify the github client path matches the catalog (not a hardcoded string) + const expected = githubClient() + expect(capturedCtx.github!.issues.path({ owner: 'AgentWorkforce', repo: 'factory' })) + .toBe(expected.issues.path({ owner: 'AgentWorkforce', repo: 'factory' })) + expect(capturedCtx.github!.issues.path({ owner: 'AgentWorkforce', repo: 'factory' })) + .toBe('/github/repos/AgentWorkforce/factory/issues') + }) + + it('exposes dynamic relay-helpers clients for non-named providers', async () => { + const { runner, calls } = makeRunner() + const registry = createRelayflowPolicyRegistry() + + registry.register('notion.page.created', { + templatePath: 'workflows/notion-page.yaml', + mapInputs: (_event, ctx) => ({ + relayProvider: ctx.relayClient('notion').provider, + relayPath: ctx.relayClient('notion').path('pages', { pageId: 'page-1' }), + providerPath: ctx.providerClient('notion').pages.path({ pageId: 'page-1' }), + }), + }) + + await dispatchRelayflowForTrigger( + { trigger: 'notion.page.created', resourceId: 'page-1' }, + registry, + { cwd: '/work', mountRoot: '/mnt/.integrations', workflowRunner: runner }, + ) + + expect(calls[0]!.inputs).toEqual({ + relayProvider: 'notion', + relayPath: '/notion/pages/page-1/meta.json', + providerPath: '/notion/pages/page-1/meta.json', + }) + }) + + it('propagates the workflow runner status and run id on needs_human outcome', async () => { + const { runner } = makeRunner('needs_human', 'run-waiting') + const registry = createRelayflowPolicyRegistry() + registry.register('linear.issue.created', { + templatePath: 'workflows/triage.yaml', + mapInputs: () => ({}), + }) + + const result = await dispatchRelayflowForTrigger( + { trigger: 'linear.issue.created' }, + registry, + { cwd: '/work', workflowRunner: runner }, + ) + + expect(result!.status).toBe('needs_human') + expect(result!.runId).toBe('run-waiting') + }) +}) + +describe('triggerEventFromChangeEvent', () => { + it('normalizes Relayfile change events into policy trigger names', () => { + const change = { + id: 'evt-1', + workspace: 'factory-test', + type: 'file.created', + occurredAt: '2026-06-27T00:00:00.000Z', + resource: { + path: '/github/repos/AgentWorkforce/factory/pulls/42/meta.json', + kind: 'file', + id: '42', + provider: 'github', + }, + summary: {}, + expand: async () => ({ level: 'summary', path: '/github/repos/AgentWorkforce/factory/pulls/42/meta.json', summary: {} }), + } as unknown as ChangeEvent + + expect(triggerEventFromChangeEvent(change)).toMatchObject({ + trigger: 'github.pull_request.created', + resourceId: '42', + payload: { + path: '/github/repos/AgentWorkforce/factory/pulls/42/meta.json', + provider: 'github', + resource: 'pull_request', + }, + }) + }) +}) diff --git a/src/dispatch/relayflow-registry.ts b/src/dispatch/relayflow-registry.ts new file mode 100644 index 0000000..8946361 --- /dev/null +++ b/src/dispatch/relayflow-registry.ts @@ -0,0 +1,221 @@ +import { + githubClient, + linearClient, + providerClient, + relayClient, + slackClient, + type GithubClient, + type IntegrationClientOptions, + type LinearClient, + type ProviderClient, + type RelayClient, + type SlackClient, +} from '@relayfile/relay-helpers' + +import { runRelayflowsWorkflow, type WorkflowRunner, type WorkflowRunnerResult } from '../node/factory-node.js' +import type { ChangeEvent } from '../ports/mount.js' + +// Normalized trigger string: "{provider}.{resource}.{action}" +// Examples: "linear.issue.created", "github.pull_request.review_submitted" +export type IntegrationTrigger = string + +export interface TriggerEvent { + trigger: IntegrationTrigger + // Provider-specific resource identifier (e.g. issue UUID, PR number as string) + resourceId?: string + // Raw event payload forwarded from the integration webhook + payload?: Record +} + +// Relay-helpers-backed provider clients injected into every input mapper. +// Paths come from the writeback-path catalog so they never drift from the +// adapters that materialize the draft — no hardcoded provider path strings. +export interface TriggerMapperContext { + linear: LinearClient + github: GithubClient + slack: SlackClient + relayClient: typeof relayClient + providerClient: typeof providerClient +} + +// Maps a trigger event to a structured input record for the relayflow template. +// May be async to allow reading provider state via relay-helpers before shaping inputs. +export type TriggerInputMapper = ( + event: TriggerEvent, + ctx: TriggerMapperContext, +) => Promise> | Record + +export interface RelayflowPolicyEntry { + // Path to the relayflow template (.yaml, .yml, .ts, or .py). + // Relative paths are resolved against the cwd passed to dispatchRelayflowForTrigger. + templatePath: string + mapInputs: TriggerInputMapper +} + +export interface RelayflowDispatchResult { + trigger: string + templatePath: string + runId?: string + status: WorkflowRunnerResult['status'] + inputs: Record +} + +/** + * Registry mapping normalized integration triggers to relayflow template paths + * and input mappers. Configurable at construction time — register policies via + * `register()` and extend or override entries freely; no hardcoded switch inside + * any node handler. + */ +export class RelayflowPolicyRegistry { + readonly #entries = new Map() + + register(trigger: IntegrationTrigger, entry: RelayflowPolicyEntry): this { + this.#entries.set(trigger, entry) + return this + } + + resolve(trigger: IntegrationTrigger): RelayflowPolicyEntry | undefined { + return this.#entries.get(trigger) + } + + get size(): number { + return this.#entries.size + } + + triggers(): IterableIterator { + return this.#entries.keys() + } +} + +export function createRelayflowPolicyRegistry(): RelayflowPolicyRegistry { + return new RelayflowPolicyRegistry() +} + +export interface DispatchRelayflowOptions { + // Working directory passed to the workflow runner (resolves relative templatePaths). + cwd: string + // Mount root for the relay-helpers VFS clients (provider read/write operations). + // Defaults to the process ambient mount root when omitted. + mountRoot?: string + // Override the workflow runner (defaults to runRelayflowsWorkflow). + workflowRunner?: WorkflowRunner +} + +export type RelayflowDynamicClient = RelayClient[0]> +export type RelayflowDynamicProviderClient = ProviderClient[0]> + +export function triggerEventFromChangeEvent(event: ChangeEvent): TriggerEvent | null { + const path = event.resource?.path + const provider = event.resource?.provider ?? providerFromPath(path) + const resource = resourceFromPath(provider, path) + if (!provider || !resource) return null + + return { + trigger: `${provider}.${resource}.${actionFromChangeEvent(event)}`, + resourceId: event.resource?.id ?? path, + payload: { + event, + path, + provider, + resource, + }, + } +} + +export async function dispatchRelayflowForChangeEvent( + changeEvent: ChangeEvent, + registry: RelayflowPolicyRegistry, + opts: DispatchRelayflowOptions, +): Promise { + const event = triggerEventFromChangeEvent(changeEvent) + if (!event) return null + return dispatchRelayflowForTrigger(event, registry, opts) +} + +/** + * Look up `event.trigger` in `registry`, build relay-helpers-backed provider + * clients, call the entry's `mapInputs`, and invoke the relayflows SDK. + * + * Returns null when no policy is registered for the trigger (no dispatch, no + * error). Returns a {@link RelayflowDispatchResult} with the run id and status + * on success. + */ +export async function dispatchRelayflowForTrigger( + event: TriggerEvent, + registry: RelayflowPolicyRegistry, + opts: DispatchRelayflowOptions, +): Promise { + const entry = registry.resolve(event.trigger) + if (!entry) return null + + const clientOpts: IntegrationClientOptions = opts.mountRoot ? { mountRoot: opts.mountRoot } : {} + const ctx: TriggerMapperContext = { + linear: linearClient(clientOpts), + github: githubClient(clientOpts), + slack: slackClient(clientOpts), + relayClient: (provider) => relayClient(provider, clientOpts), + providerClient: (provider) => providerClient(provider, clientOpts), + } + + const inputs = await entry.mapInputs(event, ctx) + const runner = opts.workflowRunner ?? runRelayflowsWorkflow + + const result = await runner({ + workflow: entry.templatePath, + cwd: opts.cwd, + inputs, + }) + + return { + trigger: event.trigger, + templatePath: entry.templatePath, + runId: result.runId, + status: result.status, + inputs, + } +} + +function providerFromPath(path: string | undefined): string | undefined { + return path?.split('/').filter(Boolean)[0] +} + +function resourceFromPath(provider: string | undefined, path: string | undefined): string | undefined { + if (!path) return undefined + const segments = path.split('/').filter(Boolean) + if (segments.length < 2) return undefined + + if (provider === 'github') { + const reposIndex = segments.indexOf('repos') + const collection = reposIndex >= 0 ? segments[reposIndex + 3] : undefined + return normalizeResourceName(collection ?? segments[1]) + } + + if (provider === 'slack') { + const messageIndex = segments.findIndex((segment) => segment === 'messages' || segment === 'replies') + if (messageIndex >= 0) return normalizeResourceName(segments[messageIndex]) + } + + return normalizeResourceName(segments[1]) +} + +function normalizeResourceName(value: string | undefined): string | undefined { + if (!value) return undefined + if (value === 'pulls' || value === 'pull-requests' || value === 'pull_requests') return 'pull_request' + if (value.endsWith('ies')) return `${value.slice(0, -3)}y` + if (value.endsWith('s')) return value.slice(0, -1) + return value +} + +function actionFromChangeEvent(event: ChangeEvent): string { + const filesystemEventType = (event as ChangeEvent & { filesystemEventType?: string }).filesystemEventType + return actionFromChangeEventType(filesystemEventType ?? event.type) +} + +function actionFromChangeEventType(type: ChangeEvent['type'] | string): string { + const eventType = String(type) + if (eventType === 'file.created') return 'created' + if (eventType === 'file.updated') return 'updated' + if (eventType === 'file.deleted') return 'deleted' + const parts = eventType.split('.').filter(Boolean) + return parts.at(-1) ?? 'changed' +} diff --git a/src/index.ts b/src/index.ts index 26ed2ab..f1353eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,24 @@ export type { TemplateIssue, TemplateRoute, } from './dispatch/templates' +export { + createRelayflowPolicyRegistry, + dispatchRelayflowForChangeEvent, + dispatchRelayflowForTrigger, + RelayflowPolicyRegistry, + triggerEventFromChangeEvent, +} from './dispatch/relayflow-registry' +export type { + DispatchRelayflowOptions, + IntegrationTrigger, + RelayflowDynamicClient, + RelayflowDynamicProviderClient, + RelayflowDispatchResult, + RelayflowPolicyEntry, + TriggerEvent, + TriggerInputMapper, + TriggerMapperContext, +} from './dispatch/relayflow-registry' export { createFleet } from './fleet/create-fleet' export type { CreateFleetDeps, @@ -215,6 +233,7 @@ export type { FactoryLoopRunOptions, FactoryLiveSubscriptionOptions, FactoryPorts, + FactoryRelayflowDispatchPort, FactoryStartOptions, FactoryStatus, IssueRef, diff --git a/src/node/factory-node.test.ts b/src/node/factory-node.test.ts index 3c8d8b3..7540ac0 100644 --- a/src/node/factory-node.test.ts +++ b/src/node/factory-node.test.ts @@ -11,6 +11,7 @@ import { factoryNodeInventorySync, FACTORY_NODE_CONFIG_ENV, parseFactoryNodeConfig, + runRelayflowsWorkflow, resolveFactoryNodeConfigPath, type WorkflowRunnerInput, } from './factory-node' @@ -159,7 +160,7 @@ describe('factory node definition', () => { expect(spawns[0].agent.harness_config.args.join('\n')).toContain('mcp_servers.agent-relay.command="/usr/local/bin/node"') }) - it('runs workflow:run by shelling out to relayflows in the mapped checkout', async () => { + it('runs workflow:run through the Relayflows SDK in the mapped checkout', async () => { const calls: WorkflowRunnerInput[] = [] const { ctx } = fakeContext('workflow-inv') const definition = createFactoryNodeDefinition({ @@ -167,12 +168,11 @@ describe('factory node definition', () => { workflowRunner: async (input) => { calls.push(input) return { - command: 'relayflows', - args: ['run', input.workflow], cwd: input.cwd, - exitCode: 0, - stdout: 'ok', - stderr: '', + runner: '@relayflows/core', + status: 'completed', + runId: 'run-1', + workflowName: 'default', } }, }) @@ -186,11 +186,10 @@ describe('factory node definition', () => { workflow: 'workflows/factory/linear-issue.ts', cwd: '/work/relay', invocationId: 'workflow-inv', - command: 'relayflows', - args: ['run', 'workflows/factory/linear-issue.ts'], - exitCode: 0, - stdout: 'ok', - stderr: '', + runner: '@relayflows/core', + status: 'completed', + runId: 'run-1', + workflowName: 'default', }) expect(calls).toEqual([{ workflow: 'workflows/factory/linear-issue.ts', @@ -200,6 +199,39 @@ describe('factory node definition', () => { }]) }) + it('runs the default Relayflows SDK runner without a CLI binary', async () => { + const cwd = mkdtempSync(join(tmpdir(), 'factory-relayflows-sdk-')) + writeFileSync(join(cwd, 'workflow.yaml'), [ + "version: '1.0'", + 'name: sdk-smoke', + 'swarm:', + ' pattern: pipeline', + 'workflows:', + ' - name: default', + ' steps:', + ' - name: verify', + ' type: deterministic', + ' command: printf "%s" "{{issue}}" | grep -q "AR-13"', + ' failOnError: true', + '', + ].join('\n')) + + const result = await runRelayflowsWorkflow({ + workflow: 'workflow.yaml', + cwd, + inputs: { issue: 'AR-13' }, + invocationId: 'sdk-inv', + }) + + expect(result).toMatchObject({ + cwd, + runner: '@relayflows/core', + status: 'completed', + workflowName: 'default', + }) + expect(result.runId).toBeTruthy() + }) + it('rejects an action that asks for a checkout not advertised by NodeConfig', async () => { const { ctx } = fakeContext() const definition = createFactoryNodeDefinition({ config: nodeConfig() }) diff --git a/src/node/factory-node.ts b/src/node/factory-node.ts index 4c4ea79..a1a61b3 100644 --- a/src/node/factory-node.ts +++ b/src/node/factory-node.ts @@ -1,12 +1,12 @@ import { readFileSync } from 'node:fs' -import { createRequire } from 'node:module' import { hostname } from 'node:os' -import { basename, dirname, join, resolve, sep } from 'node:path' -import { spawn as spawnProcess } from 'node:child_process' +import { basename, extname, join, resolve, sep } from 'node:path' import { action, defineNode, type FleetActionContext, type FleetCapabilityValue, type FleetNodeDefinition } from '@agent-relay/fleet' import type { AgentSpec, JsonValue, RestartPolicy } from '@agent-relay/harness-driver/protocol' import type { SpawnPtyInput } from '@agent-relay/harness-driver' +import { runScriptWorkflow, runWorkflow } from '@relayflows/core' +import type { WorkflowRunRow } from '@relayflows/core' import { z } from 'zod' import { loadFactoryConfig, NodeConfigSchema, type NodeConfig } from '../config/schema' @@ -26,7 +26,6 @@ const factoryNodeCapabilities = ['spawn:claude', 'spawn:codex', 'workflow:run'] type FactoryNodeCapability = (typeof factoryNodeCapabilities)[number] const knownFactoryNodeCapabilities = new Set(factoryNodeCapabilities) -const requireForResolve = createRequire(import.meta.url) const restartPolicySchema: z.ZodType = z.object({ enabled: z.boolean().optional(), @@ -109,12 +108,12 @@ export interface WorkflowRunnerInput { } export interface WorkflowRunnerResult { - command: string - args: string[] cwd: string - exitCode: number - stdout: string - stderr: string + runner: '@relayflows/core' + status: WorkflowRunRow['status'] | 'completed' + runId?: string + workflowName?: string + result?: WorkflowRunRow } export type WorkflowRunner = (input: WorkflowRunnerInput) => Promise @@ -292,52 +291,68 @@ async function runWorkflowCapability( } export async function runRelayflowsWorkflow(input: WorkflowRunnerInput): Promise { - const { command, args } = resolveRelayflowsInvocation(input.workflow) - const childEnv = { - ...process.env, + const workflowPath = resolve(input.cwd, input.workflow) + const env = { RELAYFLOWS_INPUTS_JSON: JSON.stringify(input.inputs), FACTORY_WORKFLOW_INPUTS_JSON: JSON.stringify(input.inputs), ...(input.invocationId ? { RELAY_INVOCATION_ID: input.invocationId } : {}), } + const ext = extname(workflowPath).toLowerCase() - return await new Promise((resolvePromise, reject) => { - const child = spawnProcess(command, args, { + if (ext === '.yaml' || ext === '.yml') { + const result = await withProcessEnv(env, () => runWorkflow(workflowPath, { cwd: input.cwd, - env: childEnv, - stdio: ['ignore', 'pipe', 'pipe'], - }) - let stdout = '' - let stderr = '' - child.stdout?.setEncoding('utf8') - child.stderr?.setEncoding('utf8') - child.stdout?.on('data', (chunk) => { - stdout += chunk - }) - child.stderr?.on('data', (chunk) => { - stderr += chunk - }) - child.on('error', reject) - child.on('close', (code) => { - const exitCode = code ?? 1 - const result = { command, args, cwd: input.cwd, exitCode, stdout, stderr } - if (exitCode === 0) { - resolvePromise(result) - return - } - reject(new Error(`relayflows run ${input.workflow} failed with exit code ${exitCode}${stderr ? `: ${stderr.trim()}` : ''}`)) - }) - }) + vars: input.inputs, + })) + if (result.status !== 'completed' && result.status !== 'needs_human') { + throw new Error(`Relayflows workflow ${input.workflow} ${result.status}${result.error ? `: ${result.error}` : ''}`) + } + return { + cwd: input.cwd, + runner: '@relayflows/core', + status: result.status, + runId: result.id, + workflowName: result.workflowName, + result, + } + } + + if (ext === '.ts' || ext === '.tsx' || ext === '.py') { + await withProcessEnv(env, () => runScriptWorkflowWithCwd(workflowPath, input.cwd)) + return { + cwd: input.cwd, + runner: '@relayflows/core', + status: 'completed', + } + } + + throw new Error(`Unsupported workflow file type: ${ext || '(none)'}. Use .yaml, .yml, .ts, .tsx, or .py`) } -function resolveRelayflowsInvocation(workflow: string): { command: string; args: string[] } { - if (process.env.RELAYFLOWS_BIN) { - return { command: process.env.RELAYFLOWS_BIN, args: ['run', workflow] } +function runScriptWorkflowWithCwd(workflowPath: string, cwd: string): Promise { + const runner = runScriptWorkflow as (filePath: string, options?: { cwd?: string }) => Promise + return runner(workflowPath, { cwd }) +} + +async function withProcessEnv( + values: Record, + fn: () => Promise, +): Promise { + const previous = new Map() + for (const [key, value] of Object.entries(values)) { + previous.set(key, process.env[key]) + process.env[key] = value } try { - const packageJsonPath = requireForResolve.resolve('@relayflows/cli/package.json') - return { command: process.execPath, args: [join(dirname(packageJsonPath), 'dist', 'cli.js'), 'run', workflow] } - } catch { - return { command: 'relayflows', args: ['run', workflow] } + return await fn() + } finally { + for (const [key, value] of previous) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } } } diff --git a/src/orchestrator/factory.test.ts b/src/orchestrator/factory.test.ts index 48d258e..2812ff7 100644 --- a/src/orchestrator/factory.test.ts +++ b/src/orchestrator/factory.test.ts @@ -10,6 +10,7 @@ import { checkFactoryLoopLiveness, closeProbePr, createFactory, + createRelayflowPolicyRegistry, isInFactoryScope, parseLinearIssue, readFactoryInFlightRegistry, @@ -18,6 +19,7 @@ import { type FactoryConfig, type TriageDecision, type TriageEngine, + type WorkflowRunnerInput, } from '../index' import { changeEventPath } from './factory' import type { ChangeEvent, EventPage, LinearWriteback, ProviderSyncStatus, SlackWriteback, SpawnInput, SpawnResult } from '../ports' @@ -3867,6 +3869,69 @@ describe('FactoryLoop', () => { await factory.stop() }) + it('dispatches relayflow policies from subscribe events without polling', async () => { + const mount = new CountingEventsMount() + const fleet = new FakeFleetClient() + const registry = createRelayflowPolicyRegistry() + const workflowCalls: WorkflowRunnerInput[] = [] + registry.register('notion.page.changed', { + templatePath: 'workflows/notion-page.yaml', + mapInputs: (event) => ({ + pageId: event.resourceId, + sourcePath: event.payload?.path, + }), + }) + const factory = createFactory(config(), { + mount, + fleet, + triage: new StaticTriage(), + relayflows: { + registry, + cwd: '/work/factory', + mountRoot: '/mnt/.integrations', + workflowRunner: async (input) => { + workflowCalls.push(input) + return { + cwd: input.cwd, + runner: '@relayflows/core', + status: 'completed', + runId: 'run-notion-page', + } + }, + }, + }) + + await factory.start({ mode: 'live', liveSubscription: { transport: 'subscribe' } }) + mount.emit({ + id: 'event-relayflow-notion', + workspace: 'factory-test', + type: 'relayfile.changed', + occurredAt: new Date().toISOString(), + resource: { + path: '/notion/pages/page-1.json', + kind: 'file', + id: 'page-1', + provider: 'notion', + }, + summary: {}, + expand: async () => ({ level: 'summary', path: '/notion/pages/page-1.json', summary: {} }), + } as unknown as ChangeEvent) + + await vi.waitFor(() => expect(workflowCalls).toHaveLength(1)) + + expect(mount.getEventsCalls).toBe(0) + expect(workflowCalls[0]).toEqual({ + workflow: 'workflows/notion-page.yaml', + cwd: '/work/factory', + inputs: { + pageId: 'page-1', + sourcePath: '/notion/pages/page-1.json', + }, + }) + expect(factory.status().counters.relayflowEventsDispatched).toBe(1) + await factory.stop() + }) + it('live subscription runs a startup full pull when the high-watermark route is unavailable', async () => { const path = issuePath(40) const mount = new RouteNotFoundCountingListTreeMount({ [path]: realIssueFile(40) }) diff --git a/src/orchestrator/factory.ts b/src/orchestrator/factory.ts index 099d874..09168ea 100644 --- a/src/orchestrator/factory.ts +++ b/src/orchestrator/factory.ts @@ -26,6 +26,7 @@ import type { Clock, Logger } from '../ports/system' import { InMemoryStateStore } from '../state/in-memory-state-store' import { containsExplicitIssueReference, containsIssueKey } from '../issue-key-match' import { isInFactoryScope } from '../safety/factory-scope' +import { dispatchRelayflowForChangeEvent } from '../dispatch/relayflow-registry' import { deriveDescriptorsFromMount, prescriptiveInstructions, @@ -187,6 +188,7 @@ export class FactoryLoop implements Factory { readonly #terminationGraceMs: number | undefined readonly #state: StateStore readonly #workspaceId: string + readonly #relayflows?: FactoryPorts['relayflows'] #batchView?: BatchSnapshot #batchReady: Promise readonly #listeners = new Map>() @@ -292,6 +294,7 @@ export class FactoryLoop implements Factory { this.#readChildPids = ports.readChildPids this.#terminationGraceMs = ports.terminationGraceMs this.#workspaceId = config.workspaceId ?? 'default' + this.#relayflows = ports.relayflows this.#state = ports.stateStore ?? new InMemoryStateStore({ batchSize: config.batchSize, agentQuestionDedupeLimit: AGENT_QUESTION_DEDUPE_LIMIT, @@ -412,6 +415,7 @@ export class FactoryLoop implements Factory { await this.#backfillReadyIssues() this.#subscription = this.#mount.subscribe([`${ISSUE_ROOT}/**/*.json`, LIVE_GITHUB_ISSUE_GLOB], (event) => { + void this.#dispatchRelayflowEvent(event) // The SDK types `resource` as always-present, but the polling fallback and // degraded-sync paths can deliver events without it. Skip those rather // than throwing (which would otherwise crash the subscription handler). @@ -733,6 +737,7 @@ export class FactoryLoop implements Factory { const batch = events.splice(0, LIVE_EVENT_DRAIN_BATCH_SIZE) const paths: string[] = [] for (const event of batch) { + await this.#dispatchRelayflowEvent(event) const path = await this.#prepareLiveEventForDrain(event, seenIssueKeys) if (path) { paths.push(path) @@ -872,6 +877,35 @@ export class FactoryLoop implements Factory { return path } + async #dispatchRelayflowEvent(event: ChangeEvent): Promise { + const relayflows = this.#relayflows + if (!relayflows) return + + try { + const result = await dispatchRelayflowForChangeEvent(event, relayflows.registry, { + cwd: relayflows.cwd ?? process.cwd(), + mountRoot: relayflows.mountRoot ?? this.#integrationsMountRoot(), + workflowRunner: relayflows.workflowRunner, + }) + if (!result) return + + this.#increment('relayflowEventsDispatched') + this.#logger.info?.('[factory] relayflow dispatched for integration event', { + trigger: result.trigger, + templatePath: result.templatePath, + runId: result.runId, + status: result.status, + }) + } catch (error) { + this.#increment('relayflowDispatchErrors') + this.#logger.warn?.('[factory] relayflow dispatch failed for integration event', { + eventId: event.id, + path: changeEventPath(event), + error, + }) + } + } + async #handlePreparedLiveChange(path: string): Promise { if (isGithubPullFilePath(path)) { await this.#handlePrChange(path) diff --git a/src/ports/fleet.ts b/src/ports/fleet.ts index 30dd5a3..e2c21cf 100644 --- a/src/ports/fleet.ts +++ b/src/ports/fleet.ts @@ -1,5 +1,5 @@ // `workflow:run` is a fleet node capability. The node-side Phase 4 handler -// shells out to `relayflows run ` in that node's repo checkout; the +// invokes the Relayflows SDK in that node's repo checkout; the // factory only emits the workflow path and inputs through the relay fleet. export type Capability = 'spawn:codex' | 'spawn:claude' | 'workflow:run' export type RestartPolicy = import('@agent-relay/harness-driver').SpawnPtyInput['restartPolicy'] diff --git a/src/subscriptions/__tests__/event-client.test.ts b/src/subscriptions/__tests__/event-client.test.ts index 2ca8cd1..8c36916 100644 --- a/src/subscriptions/__tests__/event-client.test.ts +++ b/src/subscriptions/__tests__/event-client.test.ts @@ -3,10 +3,12 @@ import { describe, expect, it, vi } from 'vitest' import { createWorkspaceScopedEventClient, filesystemEventToChangeEvent, + type ChangeEvent, type FilesystemEventLike, type RelayFileSyncLike, type WorkspaceEventClientSource, } from '../event-client' +import { triggerEventFromChangeEvent } from '../../dispatch/relayflow-registry' type FakeSyncState = 'idle' | 'connecting' | 'open' | 'polling' | 'reconnecting' | 'closed' type FakeSyncHandler = @@ -75,6 +77,8 @@ describe('workspace scoped event client', () => { })) expect(change.id).toBe('ws-1:/linear/issues/AR-1__uuid.json:7') + expect(change.type).toBe('relayfile.changed') + expect(change.filesystemEventType).toBe('file.deleted') expect(change.resource).toMatchObject({ path: '/linear/issues/AR-1__uuid.json', provider: 'linear', @@ -87,6 +91,42 @@ describe('workspace scoped event client', () => { }) }) + it('preserves created action semantics through the live subscription callback', async () => { + const sync = new FakeSync() + const changes: ChangeEvent[] = [] + const client = { + getEvents: vi.fn(), + getResourceAtEvent: vi.fn(), + } as unknown as WorkspaceEventClientSource + + createWorkspaceScopedEventClient( + client, + 'ws-1', + async () => undefined, + 'https://relayfile.invalid', + () => sync, + ).subscribe(['/linear/issues/**'], (change) => { + changes.push(change) + }, { coalesce: 'none' }) + + await Promise.resolve() + sync.emit(event({ + eventId: 'evt-created', + type: 'file.created', + path: '/linear/issues/AR-2__uuid.json', + revision: '1', + })) + await Promise.resolve() + + expect(changes).toHaveLength(1) + expect(changes[0]!.type).toBe('relayfile.changed') + expect(changes[0]!.filesystemEventType).toBe('file.created') + expect(triggerEventFromChangeEvent(changes[0]!)).toMatchObject({ + trigger: 'linear.issue.created', + resourceId: 'AR-2__uuid.json', + }) + }) + it('coalesces repeated path updates and reports logger/telemetry events', async () => { vi.useFakeTimers() try { diff --git a/src/subscriptions/event-client.ts b/src/subscriptions/event-client.ts index ee8d5d6..7d54268 100644 --- a/src/subscriptions/event-client.ts +++ b/src/subscriptions/event-client.ts @@ -17,6 +17,7 @@ import { globMatchesPath, relayfileSdkPathFiltersFor } from './globs' export type ChangeEvent = Omit & { type: SdkChangeEvent['type'] | FilesystemEventType | 'relayfile.changed.summary' + filesystemEventType?: FilesystemEventType origin?: FilesystemEvent['origin'] expand: (level?: ExpansionLevel) => Promise } @@ -152,6 +153,7 @@ export function filesystemEventToChangeEvent( id: event.eventId || `${workspaceId}:${path}:${event.revision}`, workspace: workspaceId, type: 'relayfile.changed', + filesystemEventType: event.type, occurredAt: event.timestamp || new Date().toISOString(), resource: { path, diff --git a/src/types.ts b/src/types.ts index 805b287..47fa0de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,7 @@ import type { Clock, Logger } from './ports/system' import type { CloseProbePrInput, CloseProbePrResult } from './github/probe-closer' import type { GhRunner, GithubMergeGate } from './github/merge-gate' import type { AgentProcessFinder, ProcessIdentity } from './orchestrator/process-identity' +import type { DispatchRelayflowOptions, RelayflowPolicyRegistry } from './dispatch/relayflow-registry' export interface FactoryPorts { mount: MountClient @@ -30,6 +31,12 @@ export interface FactoryPorts { kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean readChildPids?: (pid: number) => Promise terminationGraceMs?: number + relayflows?: FactoryRelayflowDispatchPort +} + +export interface FactoryRelayflowDispatchPort extends Omit { + registry: RelayflowPolicyRegistry + cwd?: string } export interface Factory { From 629c65281b9c1fb9e4c17591dd258223fca1fefd Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sat, 27 Jun 2026 21:56:23 +0200 Subject: [PATCH 2/3] fix(factory): address relayflow dispatch review feedback --- src/cli/fleet.ts | 2 +- src/node/factory-node.ts | 36 ++++++++++++------- src/orchestrator/factory.test.ts | 61 ++++++++++++++++++++++++++++++++ src/orchestrator/factory.ts | 53 ++++++++++++++++----------- 4 files changed, 118 insertions(+), 34 deletions(-) diff --git a/src/cli/fleet.ts b/src/cli/fleet.ts index 7541757..e7fdb5d 100644 --- a/src/cli/fleet.ts +++ b/src/cli/fleet.ts @@ -394,7 +394,7 @@ async function ensureClonePathMounts( const mountFn = deps.ensureLocalMount ?? ensureLocalMount const mountOpts = { acceptableWorkspaceIds: acceptableMountIds } const daemonCwd = resolve(process.cwd()) - for (const clonePath of new Set(Object.values(config.clonePaths))) { + for (const clonePath of new Set(Object.values(config.clonePaths ?? {}))) { const resolved = resolve(clonePath) if (resolved !== daemonCwd) { try { diff --git a/src/node/factory-node.ts b/src/node/factory-node.ts index a1a61b3..22abb28 100644 --- a/src/node/factory-node.ts +++ b/src/node/factory-node.ts @@ -26,6 +26,7 @@ const factoryNodeCapabilities = ['spawn:claude', 'spawn:codex', 'workflow:run'] type FactoryNodeCapability = (typeof factoryNodeCapabilities)[number] const knownFactoryNodeCapabilities = new Set(factoryNodeCapabilities) +let workflowEnvLock: Promise = Promise.resolve() const restartPolicySchema: z.ZodType = z.object({ enabled: z.boolean().optional(), @@ -338,22 +339,31 @@ async function withProcessEnv( values: Record, fn: () => Promise, ): Promise { - const previous = new Map() - for (const [key, value] of Object.entries(values)) { - previous.set(key, process.env[key]) - process.env[key] = value - } - try { - return await fn() - } finally { - for (const [key, value] of previous) { - if (value === undefined) { - delete process.env[key] - } else { - process.env[key] = value + const run = async (): Promise => { + const previous = new Map() + for (const [key, value] of Object.entries(values)) { + previous.set(key, process.env[key]) + process.env[key] = value + } + try { + return await fn() + } finally { + for (const [key, value] of previous) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } } } } + + const result = workflowEnvLock.then(run, run) + workflowEnvLock = result.then( + () => undefined, + () => undefined, + ) + return result } function resolveCheckoutPath( diff --git a/src/orchestrator/factory.test.ts b/src/orchestrator/factory.test.ts index 2812ff7..68e7690 100644 --- a/src/orchestrator/factory.test.ts +++ b/src/orchestrator/factory.test.ts @@ -537,6 +537,12 @@ class RosterPidHarnessClient implements HarnessDriverClientLike { class CountingEventsMount extends FakeMountClient { getEventsCalls = 0 + subscribeGlobs: string[][] = [] + + override subscribe(...args: Parameters): ReturnType { + this.subscribeGlobs.push([...args[0]]) + return super.subscribe(...args) + } override async getEvents(opts: { cursor?: string; limit?: number; provider?: string; last?: number }): Promise { this.getEventsCalls += 1 @@ -3902,6 +3908,7 @@ describe('FactoryLoop', () => { }) await factory.start({ mode: 'live', liveSubscription: { transport: 'subscribe' } }) + expect(mount.subscribeGlobs.at(-1)).toEqual(['/**']) mount.emit({ id: 'event-relayflow-notion', workspace: 'factory-test', @@ -3932,6 +3939,60 @@ describe('FactoryLoop', () => { await factory.stop() }) + it('suppresses replayed relayflow integration events before dispatching workflows', async () => { + const mount = new CountingEventsMount() + const fleet = new FakeFleetClient() + const registry = createRelayflowPolicyRegistry() + const workflowCalls: WorkflowRunnerInput[] = [] + registry.register('notion.page.changed', { + templatePath: 'workflows/notion-page.yaml', + mapInputs: (relayEvent) => ({ path: relayEvent.path }), + }) + const event = (id: string) => ({ + id, + workspace: 'factory-test', + type: 'relayfile.changed', + occurredAt: new Date().toISOString(), + resource: { + path: '/notion/pages/page-1.json', + kind: 'file', + id: 'page-1', + provider: 'notion', + }, + summary: {}, + expand: async () => ({ level: 'summary', path: '/notion/pages/page-1.json', summary: {} }), + }) as unknown as ChangeEvent + mount.emit(event('10')) + const factory = createFactory(config(), { + mount, + fleet, + triage: new StaticTriage(), + relayflows: { + registry, + cwd: '/work/factory', + workflowRunner: async (input) => { + workflowCalls.push(input) + return { + cwd: input.cwd, + runner: '@relayflows/core', + status: 'completed', + } + }, + }, + }) + + await factory.start({ mode: 'live', liveSubscription: { transport: 'subscribe' } }) + mount.emit(event('10')) + await flush() + expect(workflowCalls).toHaveLength(0) + expect(factory.status().counters.liveReplayEventsSuppressedByWatermark).toBe(1) + + mount.emit(event('11')) + await vi.waitFor(() => expect(workflowCalls).toHaveLength(1)) + expect(factory.status().counters.relayflowEventsDispatched).toBe(1) + await factory.stop() + }) + it('live subscription runs a startup full pull when the high-watermark route is unavailable', async () => { const path = issuePath(40) const mount = new RouteNotFoundCountingListTreeMount({ [path]: realIssueFile(40) }) diff --git a/src/orchestrator/factory.ts b/src/orchestrator/factory.ts index 09168ea..9ded82c 100644 --- a/src/orchestrator/factory.ts +++ b/src/orchestrator/factory.ts @@ -66,6 +66,7 @@ type SlackThreadWatcher = { stop(): Promise } type TerminationRoots = { pids: number[]; status: AgentPidResolution['status'] } type ResolvedIssuePr = { repo: string; prNumber: number; draft?: boolean } type EventHighWatermarkResult = { highWatermark?: string; routeUnavailable: boolean } +type PreparedLiveEvent = { path?: string; dispatchRelayflow: boolean } type SlackReply = { channelDir: string threadTs: string @@ -114,6 +115,7 @@ const ISSUE_ROOT = '/linear/issues' const GITHUB_ISSUE_ROOT = '/github/repos' const READY_EVENTS_LIMIT = 100 const LIVE_ISSUE_GLOB = `${ISSUE_ROOT}/**` +const LIVE_RELAYFLOW_GLOB = '/**' // Subscribe broadly under /github/repos and let isGithubIssueFilePath() / // githubPullPathParts() re-validate the exact shape in the callback. // globMatchesPath() treats a non-terminal `**` as a single-segment wildcard, so @@ -414,7 +416,7 @@ export class FactoryLoop implements Factory { } await this.#backfillReadyIssues() - this.#subscription = this.#mount.subscribe([`${ISSUE_ROOT}/**/*.json`, LIVE_GITHUB_ISSUE_GLOB], (event) => { + this.#subscription = this.#mount.subscribe(this.#subscriptionGlobs([`${ISSUE_ROOT}/**/*.json`, LIVE_GITHUB_ISSUE_GLOB]), (event) => { void this.#dispatchRelayflowEvent(event) // The SDK types `resource` as always-present, but the polling fallback and // degraded-sync paths can deliver events without it. Skip those rather @@ -526,7 +528,7 @@ export class FactoryLoop implements Factory { // LIVE_GITHUB_ISSUE_GLOB is a terminal `${GITHUB_ISSUE_ROOT}/**`, so it // already covers the PR change events the babysitter consumes; pull-event // *processing* is gated on babysitter.enabled in #prepareLiveEventForDrain. - this.#subscription = this.#mount.subscribe([LIVE_ISSUE_GLOB, LIVE_GITHUB_ISSUE_GLOB], (event) => { + this.#subscription = this.#mount.subscribe(this.#subscriptionGlobs([LIVE_ISSUE_GLOB, LIVE_GITHUB_ISSUE_GLOB]), (event) => { this.#enqueueLiveEvent(event) }, { from: 'now', coalesce: 'none' }) } @@ -737,10 +739,12 @@ export class FactoryLoop implements Factory { const batch = events.splice(0, LIVE_EVENT_DRAIN_BATCH_SIZE) const paths: string[] = [] for (const event of batch) { - await this.#dispatchRelayflowEvent(event) - const path = await this.#prepareLiveEventForDrain(event, seenIssueKeys) - if (path) { - paths.push(path) + const prepared = await this.#prepareLiveEventForDrain(event, seenIssueKeys) + if (prepared.dispatchRelayflow) { + void this.#dispatchRelayflowEvent(event) + } + if (prepared.path) { + paths.push(prepared.path) } } await Promise.all(paths.map((path) => this.#handlePreparedLiveChange(path))) @@ -771,14 +775,19 @@ export class FactoryLoop implements Factory { await this.#refreshLiveHeartbeat() } - async #prepareLiveEventForDrain(event: ChangeEvent, seenIssueKeys: Set): Promise { + #subscriptionGlobs(factoryGlobs: string[]): string[] { + return this.#relayflows ? [LIVE_RELAYFLOW_GLOB] : factoryGlobs + } + + async #prepareLiveEventForDrain(event: ChangeEvent, seenIssueKeys: Set): Promise { const path = changeEventPath(event) if (!path) { - return undefined + return { dispatchRelayflow: false } } const isPullPath = isGithubPullFilePath(path) - if (!isIssueFilePath(path) && !isGithubIssueFilePath(path) && !isPullPath) { - return undefined + const isFactoryPath = isIssueFilePath(path) || isGithubIssueFilePath(path) || isPullPath + if (!isFactoryPath && !this.#relayflows) { + return { dispatchRelayflow: false } } if (isBeforeLiveCutoff(event.occurredAt, this.#liveConnectStartedAtMs, this.#liveReplaySkewMarginMs)) { @@ -791,7 +800,7 @@ export class FactoryLoop implements Factory { connectStartedAt: new Date(this.#liveConnectStartedAtMs).toISOString(), replaySkewMarginMs: this.#liveReplaySkewMarginMs, }) - return undefined + return { dispatchRelayflow: false } } if (isAtOrBeforeHighWatermark(event.id, this.#liveEventHighWatermark)) { @@ -802,7 +811,7 @@ export class FactoryLoop implements Factory { highWatermark: this.#liveEventHighWatermark, path, }) - return undefined + return { dispatchRelayflow: false } } const dedupeKey = liveEventDedupeKey(event) @@ -813,7 +822,7 @@ export class FactoryLoop implements Factory { id: event.id, path, }) - return undefined + return { dispatchRelayflow: false } } rememberLiveEvent(this.#seenLiveEvents, dedupeKey) } else { @@ -821,6 +830,10 @@ export class FactoryLoop implements Factory { this.#logger.warn?.('[factory] live issue event missing stable identity', { path }) } + if (!isFactoryPath) { + return { dispatchRelayflow: true } + } + if (isGithubIssueFilePath(path)) { const sourceKey = `github:${path}` if (seenIssueKeys.has(sourceKey)) { @@ -829,11 +842,11 @@ export class FactoryLoop implements Factory { id: event.id, path, }) - return undefined + return { dispatchRelayflow: false } } seenIssueKeys.add(sourceKey) this.#recordArrivalLatency(event) - return path + return { path, dispatchRelayflow: true } } if (isPullPath) { @@ -842,11 +855,11 @@ export class FactoryLoop implements Factory { const sourceKey = `pull:${path}` if (seenIssueKeys.has(sourceKey)) { this.#increment('liveDuplicatePrEventsSuppressed') - return undefined + return { dispatchRelayflow: false } } seenIssueKeys.add(sourceKey) this.#recordArrivalLatency(event) - return path + return { path, dispatchRelayflow: true } } const issueKey = keyFromPath(path) @@ -857,7 +870,7 @@ export class FactoryLoop implements Factory { path, issue: issueKey, }) - return undefined + return { dispatchRelayflow: false } } const issueRef = { key: issueKey, uuid: uuidFromPath(path) ?? issueKey, path } @@ -869,12 +882,12 @@ export class FactoryLoop implements Factory { path, issue: issueKey, }) - return undefined + return { dispatchRelayflow: false } } seenIssueKeys.add(issueKey) this.#recordArrivalLatency(event) - return path + return { path, dispatchRelayflow: true } } async #dispatchRelayflowEvent(event: ChangeEvent): Promise { From 92a18aec9345d2ff5c5a9b40fad6e48e0d041e6e Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sat, 27 Jun 2026 22:00:27 +0200 Subject: [PATCH 3/3] fix(factory): refresh npm lockfile for ci --- package-lock.json | 516 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 514 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21cdea0..4d0ca3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agent-relay/factory", - "version": "0.1.11", + "version": "0.1.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-relay/factory", - "version": "0.1.11", + "version": "0.1.13", "license": "Apache-2.0", "dependencies": { "@agent-relay/cloud": "^9.0.2", @@ -1498,6 +1498,24 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", @@ -7040,6 +7058,456 @@ } } }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", @@ -7067,6 +7535,50 @@ } } }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/vitest/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",