diff --git a/.env.example b/.env.example index 125ad43e98..973a82a9c8 100644 --- a/.env.example +++ b/.env.example @@ -151,6 +151,15 @@ GITEA_ALLOWED_USERS= # If not set, falls back to BOT_DISPLAY_NAME then config.botName # GITEA_BOT_MENTION=archon +# ============================================ +# Linear (PM Adapter) +# ============================================ +# Triggers workflows when Linear issues assigned to the configured user +# transition to "In Progress". Configure team→codebase mappings in +# ~/.archon/config.yaml under linear.mappings. +# LINEAR_API_KEY=lin_api_... +# LINEAR_WEBHOOK_SECRET= + # Server # PORT=3090 # Default: 3090. Uncomment to override — must match between server and Vite proxy. # HOST=0.0.0.0 # Bind address (default: 0.0.0.0). Set to 127.0.0.1 to restrict to localhost only. diff --git a/Dockerfile b/Dockerfile index 93a537525b..7d87df9457 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,6 +74,8 @@ RUN apt-get update && apt-get install -y \ postgresql-client \ # Chromium for agent-browser E2E testing (drives browser via CDP) chromium \ + # jq for bash-node scripts that parse JSON artifacts + jq \ && rm -rf /var/lib/apt/lists/* # Install GitHub CLI diff --git a/bun.lock b/bun.lock index 1c6cf3891f..47b1b68540 100644 --- a/bun.lock +++ b/bun.lock @@ -29,6 +29,7 @@ "@archon/git": "workspace:*", "@archon/isolation": "workspace:*", "@archon/paths": "workspace:*", + "@linear/sdk": "^34.0.0", "@octokit/rest": "^22.0.0", "@slack/bolt": "^4.6.0", "discord.js": "^14.16.0", @@ -288,25 +289,25 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1031.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.0", "@aws-sdk/credential-provider-node": "^3.972.31", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.30", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/token-providers": "3.1031.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.16", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ZgiSo2wslPXlv7wK4m2ULu2VfimbVRRlho0DqXhlvZGEqvtC209cMOxfPZWJ79Fz9sf0IzmWFkDtvMYjnwyLfw=="], + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1032.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-node": "^3.972.32", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/token-providers": "3.1032.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.17", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-fSRz/48As9c3DeS+9ZWd7kk9171pJntCCuehHBDeprD9CPF+C+ATaVNJ5SOLE5RIBR2IHOVTwjAgJt/nkS/6Yg=="], - "@aws-sdk/core": ["@aws-sdk/core@3.974.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.18", "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-8j+dMtyDqNXFmi09CBdz8TY6Ltf2jhfHuP6ZvG4zVjndRc6JF0aeBUbRwQLndbptFCsdctRQgdNWecy4TIfXAw=="], + "@aws-sdk/core": ["@aws-sdk/core@3.974.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.18", "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-gy/gffKz0zaHDaqRiLCdIvgHmaAL/HXuAtMcBP7euYSFx4BsbsdlfmUBJag+Gqe62z6/XuloKyQyaiH+kS3Vrg=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-WBHAMxyPdgeJY6ZGLvq9mJwzZ+GaNUROQbfdVshtMsDVBrZTj5ZuFjKclSjSHvKSHJ4Y4O2yvI/aA/hrJbYfng=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xfUt2CUZDC+Tf16A6roD1b4pk/nrXdkoLY3TEhv198AXDtBo5xUJP1zd0e8SmuKLN4PpIBX96OizZbmMlcI6oQ=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-+1DwCjjpo1WoiZTN08yGitI3nUwZUSQWVWFrW4C46HqZwACjcUQ7C66tnKPBTVxrEYYDOP11A6Afmu1L6ylt3g=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-hjNeYb6oLyHgMihra83ie0J/T2y9om3cy1qC90h9DRgvYXEoN4BCFf8bHguZjKhXunnv7YkmZRuYL5Mkk77eCA=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.30", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/credential-provider-env": "^3.972.26", "@aws-sdk/credential-provider-http": "^3.972.28", "@aws-sdk/credential-provider-login": "^3.972.30", "@aws-sdk/credential-provider-process": "^3.972.26", "@aws-sdk/credential-provider-sso": "^3.972.30", "@aws-sdk/credential-provider-web-identity": "^3.972.30", "@aws-sdk/nested-clients": "^3.996.20", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Fg1oJcoijwOZjTxdbx+ubqbQl8YEQ4Cwhjw6TWzQjuDEvQYNhnCXW2pN7eKtdTrdE4a6+5TVKGSm2I+i2BKIQg=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-env": "^3.972.27", "@aws-sdk/credential-provider-http": "^3.972.29", "@aws-sdk/credential-provider-login": "^3.972.31", "@aws-sdk/credential-provider-process": "^3.972.27", "@aws-sdk/credential-provider-sso": "^3.972.31", "@aws-sdk/credential-provider-web-identity": "^3.972.31", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-PuQ7e8WYzAPpzvFcajxf8c0LqSzakVHVlKw8M0oubk8Kf347YOCCqT1seQrHs5AdZuIh2RD9LX4O+Xa5ImEBfQ=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.30", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/nested-clients": "^3.996.20", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-nchIrrI/7dgjG1bW/DEWOJc00K9n+kkl6B8Mk0KO6d4GfWBOXlVr9uHp7CJR9FIrjmov5SGjHXG2q9XAtkRw6Q=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-bBmWDmtSpmLOZR6a0kmowBcVL1hiL8Vlap/RXeMpFd7JbWl87YcwqL6T9LH/0oBVEZXu1dUZAtojgSuZgMO5xw=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.31", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.26", "@aws-sdk/credential-provider-http": "^3.972.28", "@aws-sdk/credential-provider-ini": "^3.972.30", "@aws-sdk/credential-provider-process": "^3.972.26", "@aws-sdk/credential-provider-sso": "^3.972.30", "@aws-sdk/credential-provider-web-identity": "^3.972.30", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-99OHVQ6eZ5DTxiOWgHdjBMvLqv7xoY4jLK6nZ1NcNSQbAnYZkQNIHi/VqInc9fnmg7of9si/z+waE6YL9OQIlw=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.32", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.27", "@aws-sdk/credential-provider-http": "^3.972.29", "@aws-sdk/credential-provider-ini": "^3.972.31", "@aws-sdk/credential-provider-process": "^3.972.27", "@aws-sdk/credential-provider-sso": "^3.972.31", "@aws-sdk/credential-provider-web-identity": "^3.972.31", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-9aj0x9hGYUondBZSD0XkksAdHhOKttFw4BWpLCeggeg40qSJxGrAP++g0GCm0VqWc1WtC/NRFiAVzPCy56vmog=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-jibxNld3m+vbmQwn98hcQ+fLIVrx3cQuhZlSs1/hix48SjDS5/pjMLwpmtLD/lFnd6ve1AL4o1bZg3X1WRa2SQ=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1CZvfb1WzudWWIFAVQkd1OI/T1RxPcSvNWzNsb2BMBVsBJzBtB8dV5f2nymHVU4UqwxipdVt/DAbgdDRf33JDg=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.30", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/nested-clients": "^3.996.20", "@aws-sdk/token-providers": "3.1031.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-honYIM17F/+QSWJRE84T4u//ofqEi7rLbnwmIpu7fgFX5PML78wbtdSAy5Xwyve3TLpE9/f9zQx0aBVxSjAOPw=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/token-providers": "3.1032.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-x8Mx18S48XMl9bEEpYwmXDTvjWGPIfDadReN37Lc099/DUrlL4Zs9T9rwwggo6DkKS1aev6v+MTUx7JTa87TZQ=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.30", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/nested-clients": "^3.996.20", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-CyL4oWUlONQRN2SsYMVrA9Z3i3QfLWTQctI8tuKbjNGCVVDCnJf/yMbSJCOZgpPFRtxh7dgQwvpqwmJm+iytmw=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-zfuNMIkGfjYsHis9qytYf74Bcmq6Ji9Xwf4w53baRCI/b2otTwZv3SW1uRiJ5Di7999QzRGhHZ96+eUeo3gSOA=="], "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/eventstream-codec": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg=="], @@ -318,15 +319,15 @@ "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.30", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-retry": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-lCz6JfelhjD6Eco1urXM2rOYRaxROSqeoY6IEKx+soegFJOajmIBCMHTAWuJl25Wf9IAST+i0/yOk9G3rMV26A=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-retry": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-L+hXN2HDomlIsWSHW5DVD7ppccCeRnlHXZ5uHG34ePTjF5bm0I1fmrJLbUGiW97xRXWryit5cjdP4Sx2FwiGog=="], "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.16", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-format-url": "^3.972.10", "@smithy/eventstream-codec": "^4.2.14", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.20", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.0", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.30", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.16", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-bzPdsNQnCh6TvvUmTHLZlL8qgyME6mNiUErcRMyJPywIl1BEu2VZRShel3mUoSh89bOBEXEWtjocDMolFxd/9A=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.21", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.17", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Me3d/ua2lb2G0bQfFmvCeQQp3+nN6GSPqMxDmi/IQlQ8CrlpQ5C0JJHpz2AnOUkEFI0lBNrAL3Vnt29l44ndkA=="], "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.16", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QQI43Mxd53nBij0pm8HXC+t4IOC6gnhhZfzxE0OATQyO6QfPV4e+aTIRRuAJKA6Nig/cR8eLwPryqYTX9ZrjAQ=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1031.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.0", "@aws-sdk/nested-clients": "^3.996.20", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-zj/PvnbQK/2KJNln5K2QRI9HSsy+B4emz2gbQyUHkk6l7Lidu83P/9tfmC2cJXkcC3vdmyKH2DP3Iw/FDfKQuQ=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1032.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-n+PU8Z+gll7p3wDrH+Wo6fkt8sPrVnq30YYM6Ryga95oJlEneNMEbDHj0iqjMX3V7gaGdJo/hJWyPo4lscP+mA=="], "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], @@ -338,7 +339,7 @@ "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.16", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.30", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-ccvu0FNCI0C6OqmxI/tWn7BD8qGooWuURssiIM+6vbksFO8opXR4JOGtGYPj8QYzN/vfwNYrcK344PPbYuvzRg=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.17", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-utF5qjjbuJQuU9VdCkWl7L87sr93cApsrD+uxGfUnlafX8iyEzJrb7EZnufjThURZVTOtelRMXrblWxpefElUg=="], "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.18", "", { "dependencies": { "@smithy/types": "^4.14.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA=="], @@ -528,6 +529,8 @@ "@grammyjs/types": ["@grammyjs/types@3.26.0", "", {}, "sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A=="], + "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@hono/zod-openapi": ["@hono/zod-openapi@0.19.10", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@hono/zod-validator": "^0.7.1", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": ">=3.0.0" } }, "sha512-dpoS6DenvoJyvxtQ7Kd633FRZ/Qf74+4+o9s+zZI8pEqnbjdF/DtxIib08WDpCaWabMEJOL5TXpMgNEZvb7hpA=="], @@ -612,6 +615,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@linear/sdk": ["@linear/sdk@34.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.0", "graphql": "^15.4.0", "isomorphic-unfetch": "^3.1.0" } }, "sha512-mTPn7Vb9vHQUklymyUwnmvhfw3nQRtZy2sWhgD+ANS9plGUSw5GArxdwchevKvsk5nSE7FgyvHwhOVPlvEX8ow=="], + "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.2", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.2", "@mariozechner/clipboard-darwin-universal": "0.3.2", "@mariozechner/clipboard-darwin-x64": "0.3.2", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", "@mariozechner/clipboard-linux-x64-musl": "0.3.2", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" } }, "sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA=="], "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw=="], @@ -636,17 +641,17 @@ "@mariozechner/jiti": ["@mariozechner/jiti@2.6.5", "", { "dependencies": { "std-env": "^3.10.0", "yoctocolors": "^2.1.2" }, "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw=="], - "@mariozechner/pi-agent-core": ["@mariozechner/pi-agent-core@0.67.5", "", { "dependencies": { "@mariozechner/pi-ai": "^0.67.5" } }, "sha512-XZwAVYEja4YV3Or+Fb1fMvi/KphpaEvMcfGe1/lBNEOllDK3m6J/6MdqLJy85rettX3uKRuGjF3adDNju+LRow=="], + "@mariozechner/pi-agent-core": ["@mariozechner/pi-agent-core@0.67.68", "", { "dependencies": { "@mariozechner/pi-ai": "^0.67.68" } }, "sha512-anwFuzeUL7Qjbyih4DWY7w1zrOTrBxaz1L6+duLUuuzpHOun0EiP4KWIGTXPT5oJA7ZaeRNTyXJ7PlWfGQG33g=="], - "@mariozechner/pi-ai": ["@mariozechner/pi-ai@0.67.5", "", { "dependencies": { "@anthropic-ai/sdk": "^0.90.0", "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "1.14.1", "@sinclair/typebox": "^0.34.41", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-TgxI2seq+gIRy6oRQA/ogyj8c9vESMQEeICPKYe29hJCLkN/i7tgKnU9jIM+rcAJmtGaO4Iy0IL7wYV4g0qjsw=="], + "@mariozechner/pi-ai": ["@mariozechner/pi-ai@0.67.68", "", { "dependencies": { "@anthropic-ai/sdk": "^0.90.0", "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", "@sinclair/typebox": "^0.34.41", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-DWWQmcb3IV3mbGXmzYBScfKA6kA52n/stY029eiBikrIxVT7DGLG6n7KSvTA2R4qBSgi1iFL3nGHtwxmtIn6Lg=="], - "@mariozechner/pi-coding-agent": ["@mariozechner/pi-coding-agent@0.67.5", "", { "dependencies": { "@mariozechner/jiti": "^2.6.2", "@mariozechner/pi-agent-core": "^0.67.5", "@mariozechner/pi-ai": "^0.67.5", "@mariozechner/pi-tui": "^0.67.5", "@silvia-odwyer/photon-node": "^0.3.4", "ajv": "^8.17.1", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", "extract-zip": "^2.0.1", "file-type": "^21.1.1", "glob": "^13.0.1", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "marked": "^15.0.12", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "strip-ansi": "^7.1.0", "undici": "^7.19.1", "uuid": "^11.1.0", "yaml": "^2.8.2" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.2" }, "bin": { "pi": "dist/cli.js" } }, "sha512-U/kZ173IDmkwq7p8zKsrhb5fpxWUW53NTKXva6fyzwx3o/tGl3PzdnyxBfv7vHz15S7mgL/dpdiF/ANUV34JTw=="], + "@mariozechner/pi-coding-agent": ["@mariozechner/pi-coding-agent@0.67.68", "", { "dependencies": { "@mariozechner/jiti": "^2.6.2", "@mariozechner/pi-agent-core": "^0.67.68", "@mariozechner/pi-ai": "^0.67.68", "@mariozechner/pi-tui": "^0.67.68", "@silvia-odwyer/photon-node": "^0.3.4", "ajv": "^8.17.1", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", "extract-zip": "^2.0.1", "file-type": "^21.1.1", "glob": "^13.0.1", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "marked": "^15.0.12", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "strip-ansi": "^7.1.0", "undici": "^7.19.1", "uuid": "^11.1.0", "yaml": "^2.8.2" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.2" }, "bin": { "pi": "dist/cli.js" } }, "sha512-Bai2yUBpgjftqGvg3GNV9pxcMAatqJwQYsqM+7j39+tm+IpoW7hbMBnzXZQvykAwXIrTXpZFF5yp5ajeDI5Atg=="], - "@mariozechner/pi-tui": ["@mariozechner/pi-tui@0.67.5", "", { "dependencies": { "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", "marked": "^15.0.12", "mime-types": "^3.0.1" }, "optionalDependencies": { "koffi": "^2.9.0" } }, "sha512-e1dUhXDr2LUUkHmuVYxPubQnk3NYcZLNOinUVTYXCSTAEzgSq0vH5LMgf5/zHspi5AmncmJmc85Qf/VFmnpw7Q=="], + "@mariozechner/pi-tui": ["@mariozechner/pi-tui@0.67.68", "", { "dependencies": { "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", "marked": "^15.0.12", "mime-types": "^3.0.1" }, "optionalDependencies": { "koffi": "^2.9.0" } }, "sha512-RhcMaGz88lNOm5+9yx+YCIfXZALLbMxB2cwsoHzyOzs+OZAItw8tz6xJZPAnX0RHY+ENQEGMMDY9TF6pxxnkbA=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], - "@mistralai/mistralai": ["@mistralai/mistralai@1.14.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ=="], + "@mistralai/mistralai": ["@mistralai/mistralai@2.2.0", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-JQUGIXjFWnw/J9LpTSf/ZXwVW3Sh8FBAcfTo5QvAHqkl4CfSiIwnjRJhMoAFcP6ncCe84YPU1ncDGX+p3OXnfg=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], @@ -1622,7 +1627,7 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], + "fast-xml-builder": ["fast-xml-builder@1.1.5", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA=="], "fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], @@ -1716,7 +1721,7 @@ "grammy": ["grammy@1.42.0", "", { "dependencies": { "@grammyjs/types": "3.26.0", "abort-controller": "^3.0.0", "debug": "^4.4.3", "node-fetch": "^2.7.0" } }, "sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g=="], - "graphql": ["graphql@16.13.1", "", {}, "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ=="], + "graphql": ["graphql@15.10.2", "", {}, "sha512-1PRqdDPAmViWr4h1GVBT8RoPZfWSGZa7kDzleTilOfVIslsgf+cia3Nl95v1KDmR4iERPaT7WzQ+tN4MJmbg3w=="], "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], @@ -1872,6 +1877,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isomorphic-unfetch": ["isomorphic-unfetch@3.1.0", "", { "dependencies": { "node-fetch": "^2.6.1", "unfetch": "^4.2.0" } }, "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], @@ -2660,6 +2667,8 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unfetch": ["unfetch@4.2.0", "", {}, "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], "unified": ["unified@9.2.2", "", { "dependencies": { "bail": "^1.0.0", "extend": "^3.0.0", "is-buffer": "^2.0.0", "is-plain-obj": "^2.0.0", "trough": "^1.0.0", "vfile": "^4.0.0" } }, "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ=="], @@ -2994,6 +3003,8 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "msw/graphql": ["graphql@16.13.1", "", {}, "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ=="], + "msw/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "msw/type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], diff --git a/packages/adapters/package.json b/packages/adapters/package.json index 0e2fb23d52..f2a4fac1cf 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -7,11 +7,12 @@ "exports": { ".": "./src/index.ts", "./*": "./src/*", + "./pm/linear": "./src/pm/linear/index.ts", "./community/forge/gitea": "./src/community/forge/gitea/index.ts", "./community/forge/gitlab": "./src/community/forge/gitlab/index.ts" }, "scripts": { - "test": "bun test src/chat/ src/community/chat/ src/community/forge/gitlab/auth.test.ts src/forge/github/auth.test.ts src/utils/ && bun test src/forge/github/adapter.test.ts && bun test src/forge/github/context.test.ts && bun test src/community/forge/gitea/adapter.test.ts && bun test src/community/forge/gitlab/adapter.test.ts", + "test": "bun test src/chat/ src/community/chat/ src/community/forge/gitlab/auth.test.ts src/forge/github/auth.test.ts src/utils/ && bun test src/forge/github/adapter.test.ts && bun test src/forge/github/context.test.ts && bun test src/community/forge/gitea/adapter.test.ts && bun test src/community/forge/gitlab/adapter.test.ts && bun test src/pm/linear/adapter.test.ts", "type-check": "bun x tsc --noEmit" }, "dependencies": { @@ -19,6 +20,7 @@ "@archon/git": "workspace:*", "@archon/isolation": "workspace:*", "@archon/paths": "workspace:*", + "@linear/sdk": "^34.0.0", "@octokit/rest": "^22.0.0", "@slack/bolt": "^4.6.0", "discord.js": "^14.16.0", diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index c724683334..08f310bcb1 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -5,5 +5,8 @@ export { SlackAdapter } from './chat/slack'; // Forge adapters export { GitHubAdapter } from './forge/github'; +// PM adapters +export { LinearAdapter } from './pm/linear'; + // Community adapters export { DiscordAdapter } from './community/chat/discord'; diff --git a/packages/adapters/src/pm/linear/adapter.test.ts b/packages/adapters/src/pm/linear/adapter.test.ts new file mode 100644 index 0000000000..15c2115647 --- /dev/null +++ b/packages/adapters/src/pm/linear/adapter.test.ts @@ -0,0 +1,381 @@ +/** + * Unit tests for Linear adapter. + * + * Runs in its own bun test batch to avoid mock.module() pollution. + */ +import { describe, test, expect, mock, beforeEach } from 'bun:test'; +import { createHmac } from 'crypto'; + +const mockLogger = { + fatal: mock(() => undefined), + error: mock(() => undefined), + warn: mock(() => undefined), + info: mock(() => undefined), + debug: mock(() => undefined), + trace: mock(() => undefined), + child: mock(function (this: unknown) { + return this; + }), + bindings: mock(() => ({ module: 'test' })), + isLevelEnabled: mock(() => true), + level: 'info', +}; +mock.module('@archon/paths', () => ({ + createLogger: mock(() => mockLogger), +})); + +const mockGetOrCreateConversation = mock(async () => ({ + id: 'conv-linear-test', + platform_type: 'linear', + platform_conversation_id: 'linear/ENG/ENG-123', + codebase_id: null, + cwd: null, + isolation_env_id: null, + ai_assistant_type: 'claude', + title: null, + hidden: false, + deleted_at: null, + last_activity_at: null, + created_at: new Date(), + updated_at: new Date(), +})); +const mockUpdateConversation = mock(async () => {}); + +mock.module('@archon/core/db/conversations', () => ({ + getOrCreateConversation: mockGetOrCreateConversation, + updateConversation: mockUpdateConversation, +})); + +const mockFindCodebaseByName = mock(async () => ({ + id: 'codebase-test', + name: 'my-org/my-repo', + repository_url: 'https://github.com/my-org/my-repo', + default_cwd: '/tmp/test-repo', + ai_assistant_type: 'claude', + allow_env_keys: false, + commands: {}, + created_at: new Date(), + updated_at: new Date(), +})); + +mock.module('@archon/core/db/codebases', () => ({ + findCodebaseByName: mockFindCodebaseByName, +})); + +// `handleMessage` is the canonical orchestrator entry point. The adapter +// must route through it (not directly into the workflow executor). +const mockHandleMessage = mock(async () => undefined); + +class MockConversationLockManager {} + +mock.module('@archon/core', () => ({ + handleMessage: mockHandleMessage, + ConversationLockManager: MockConversationLockManager, +})); + +const mockCreateComment = mock(async () => ({ success: true })); +const mockIssue = mock(async (identifier: string) => ({ id: `uuid-for-${identifier}` })); +const MockLinearClient = mock(() => ({ + createComment: mockCreateComment, + issue: mockIssue, +})); + +mock.module('@linear/sdk', () => ({ + LinearClient: MockLinearClient, +})); + +import { LinearAdapter } from './adapter'; +import { ConversationLockManager } from '@archon/core'; + +const WEBHOOK_SECRET = 'test-webhook-secret'; + +function signPayload(payload: string, secret = WEBHOOK_SECRET): string { + const hmac = createHmac('sha256', secret); + return hmac.update(payload).digest('hex'); +} + +function buildIssuePayload( + overrides?: Partial<{ + action: string; + type: string; + stateType: string; + assigneeName: string; + teamKey: string; + identifier: string; + updatedFrom: Record | undefined; + }> +): string { + const defaults = { + action: 'update', + type: 'Issue', + stateType: 'started', + assigneeName: 'archon', + teamKey: 'ENG', + identifier: 'ENG-123', + updatedFrom: { stateId: 'old-state-id' }, + }; + const opts = { ...defaults, ...overrides }; + + return JSON.stringify({ + action: opts.action, + type: opts.type, + data: { + id: 'issue-uuid-123', + identifier: opts.identifier, + title: 'Implement user authentication', + description: 'Add JWT-based auth to the API.', + priority: 1, + state: { id: 'state-1', name: 'In Progress', type: opts.stateType }, + assignee: opts.assigneeName + ? { id: 'user-1', name: opts.assigneeName, displayName: opts.assigneeName } + : undefined, + team: { id: 'team-1', key: opts.teamKey, name: 'Engineering' }, + labels: [{ id: 'label-1', name: 'backend' }], + url: `https://linear.app/my-org/issue/${opts.identifier}`, + }, + updatedFrom: opts.updatedFrom, + createdAt: new Date().toISOString(), + organizationId: 'org-1', + }); +} + +const mockLockManager = { + acquireLock: mock(async (_id: string, handler: () => Promise) => { + await handler(); + return { status: 'started' }; + }), + getStats: () => ({ + active: 0, + queuedTotal: 0, + queuedByConversation: [], + maxConcurrent: 10, + activeConversationIds: [], + }), +} as unknown as ConversationLockManager; + +function createAdapter( + teamMappings: Record = { ENG: 'my-org/my-repo' } +): LinearAdapter { + return new LinearAdapter('fake-api-key', WEBHOOK_SECRET, mockLockManager, { + assignee: 'archon', + workflow: 'implement', + mappings: teamMappings, + }); +} + +describe('LinearAdapter', () => { + beforeEach(() => { + mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); + mockLogger.info.mockClear(); + mockCreateComment.mockClear(); + mockIssue.mockClear(); + mockGetOrCreateConversation.mockClear(); + mockUpdateConversation.mockClear(); + mockFindCodebaseByName.mockClear(); + mockHandleMessage.mockClear(); + (mockLockManager.acquireLock as ReturnType).mockClear(); + }); + + describe('platform interface', () => { + test('returns batch streaming mode', () => { + expect(createAdapter().getStreamingMode()).toBe('batch'); + }); + + test('returns linear platform type', () => { + expect(createAdapter().getPlatformType()).toBe('linear'); + }); + + test('ensureThread returns same conversation ID', async () => { + const id = await createAdapter().ensureThread('linear/ENG/ENG-123'); + expect(id).toBe('linear/ENG/ENG-123'); + }); + }); + + describe('signature verification', () => { + test('rejects invalid signature silently', async () => { + const adapter = createAdapter(); + await adapter.handleWebhook(buildIssuePayload(), 'invalid-signature'); + + expect(mockLogger.error).toHaveBeenCalledWith('linear.signature_verification_failed'); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + + test('accepts valid signature', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload(); + await adapter.handleWebhook(payload, signPayload(payload)); + + expect(mockLogger.error).not.toHaveBeenCalledWith('linear.signature_verification_failed'); + }); + }); + + describe('event filtering', () => { + test('ignores non-update actions', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload({ action: 'create' }); + await adapter.handleWebhook(payload, signPayload(payload)); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + + test('ignores non-Issue types', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload({ type: 'Comment' }); + await adapter.handleWebhook(payload, signPayload(payload)); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + + test('ignores updates without state change', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload({ updatedFrom: { title: 'Old title' } }); + await adapter.handleWebhook(payload, signPayload(payload)); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + + test('ignores state changes that are not to "started"', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload({ stateType: 'completed' }); + await adapter.handleWebhook(payload, signPayload(payload)); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + + test('ignores issues not assigned to target user', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload({ assigneeName: 'someone-else' }); + await adapter.handleWebhook(payload, signPayload(payload)); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + + test('assignee matching is case-insensitive', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload({ assigneeName: 'Archon' }); + await adapter.handleWebhook(payload, signPayload(payload)); + expect(mockHandleMessage).toHaveBeenCalledTimes(1); + }); + }); + + describe('codebase resolution', () => { + test('logs warning and posts comment when no team mapping exists', async () => { + const adapter = createAdapter({ OTHER: 'other-repo' }); + const payload = buildIssuePayload({ teamKey: 'ENG' }); + await adapter.handleWebhook(payload, signPayload(payload)); + + expect(mockLogger.warn).toHaveBeenCalled(); + expect(mockCreateComment).toHaveBeenCalled(); + const body = mockCreateComment.mock.calls[0]?.[0] as { body: string }; + expect(body.body).toContain('No codebase mapping found'); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + + test('logs error and posts comment when codebase not found in DB', async () => { + mockFindCodebaseByName.mockResolvedValueOnce(null); + const adapter = createAdapter(); + const payload = buildIssuePayload(); + await adapter.handleWebhook(payload, signPayload(payload)); + + expect(mockLogger.error).toHaveBeenCalled(); + expect(mockCreateComment).toHaveBeenCalled(); + const body = mockCreateComment.mock.calls[0]?.[0] as { body: string }; + expect(body.body).toContain('not found'); + expect(mockHandleMessage).not.toHaveBeenCalled(); + }); + }); + + describe('orchestrator dispatch', () => { + test('calls handleMessage with synthesized slash command and rich issueContext', async () => { + const adapter = createAdapter(); + const payload = buildIssuePayload(); + await adapter.handleWebhook(payload, signPayload(payload)); + + expect(mockGetOrCreateConversation).toHaveBeenCalledWith('linear', 'linear/ENG/ENG-123'); + expect(mockUpdateConversation).toHaveBeenCalled(); + expect(mockHandleMessage).toHaveBeenCalledTimes(1); + + const args = mockHandleMessage.mock.calls[0] as unknown[]; + expect(args[0]).toBe(adapter); + expect(args[1]).toBe('linear/ENG/ENG-123'); + expect(args[2]).toBe('/workflow run implement'); + + const ctx = args[3] as { + issueContext: string; + isolationHints: { workflowType: string; workflowId: string }; + }; + expect(ctx.issueContext).toContain('ENG-123'); + expect(ctx.issueContext).toContain('Implement user authentication'); + expect(ctx.issueContext).toContain('Urgent'); + expect(ctx.issueContext).toContain('backend'); + expect(ctx.issueContext).toContain('JWT-based auth'); + expect(ctx.isolationHints).toEqual({ + workflowType: 'issue', + workflowId: 'ENG-123', + }); + }); + + test('skips re-linking codebase when conversation already linked', async () => { + mockGetOrCreateConversation.mockResolvedValueOnce({ + id: 'conv-linear-test', + platform_type: 'linear', + platform_conversation_id: 'linear/ENG/ENG-123', + codebase_id: 'codebase-test', + cwd: null, + isolation_env_id: null, + ai_assistant_type: 'claude', + title: null, + hidden: false, + deleted_at: null, + last_activity_at: null, + created_at: new Date(), + updated_at: new Date(), + }); + const adapter = createAdapter(); + const payload = buildIssuePayload(); + await adapter.handleWebhook(payload, signPayload(payload)); + + expect(mockUpdateConversation).not.toHaveBeenCalled(); + expect(mockHandleMessage).toHaveBeenCalled(); + }); + + test('posts error comment when handleMessage throws', async () => { + mockHandleMessage.mockRejectedValueOnce(new Error('Boom')); + const adapter = createAdapter(); + const payload = buildIssuePayload(); + await adapter.handleWebhook(payload, signPayload(payload)); + + expect(mockLogger.error).toHaveBeenCalled(); + const lastCall = mockCreateComment.mock.calls[ + mockCreateComment.mock.calls.length - 1 + ]?.[0] as { body: string }; + expect(lastCall.body).toContain('Failed to dispatch workflow'); + expect(lastCall.body).toContain('Boom'); + }); + }); + + describe('sendMessage', () => { + test('resolves issue UUID via Linear SDK on demand and posts comment', async () => { + const adapter = createAdapter(); + await adapter.sendMessage('linear/ENG/ENG-123', 'Test message'); + + expect(mockIssue).toHaveBeenCalledWith('ENG-123'); + expect(mockCreateComment).toHaveBeenCalledTimes(1); + const call = mockCreateComment.mock.calls[0]?.[0] as { issueId: string; body: string }; + expect(call.issueId).toBe('uuid-for-ENG-123'); + expect(call.body).toBe('Test message'); + }); + + test('throws on malformed conversationId', async () => { + const adapter = createAdapter(); + await expect(adapter.sendMessage('slack/C123', 'x')).rejects.toThrow( + 'Invalid Linear conversationId' + ); + expect(mockCreateComment).not.toHaveBeenCalled(); + }); + + test('propagates createComment errors instead of swallowing', async () => { + mockCreateComment.mockRejectedValueOnce(new Error('Linear API down')); + const adapter = createAdapter(); + await expect(adapter.sendMessage('linear/ENG/ENG-123', 'x')).rejects.toThrow( + 'Linear API down' + ); + }); + }); +}); diff --git a/packages/adapters/src/pm/linear/adapter.ts b/packages/adapters/src/pm/linear/adapter.ts new file mode 100644 index 0000000000..6650dca543 --- /dev/null +++ b/packages/adapters/src/pm/linear/adapter.ts @@ -0,0 +1,257 @@ +/** + * Linear platform adapter — triggers a workflow when an issue assigned to the + * configured user transitions to "In Progress". Routes through the standard + * orchestrator entry point (`handleMessage`) so paused-run handling, slash + * command processing, and any future orchestrator behaviour apply uniformly. + */ +import { LinearClient } from '@linear/sdk'; +import type { IPlatformAdapter, MessageMetadata, LinearConfig } from '@archon/core'; +import type { IsolationHints } from '@archon/isolation'; +import { ConversationLockManager, handleMessage } from '@archon/core'; +import * as db from '@archon/core/db/conversations'; +import * as codebaseDb from '@archon/core/db/codebases'; +import { createLogger } from '@archon/paths'; +import { verifyLinearSignature, isTargetAssignee } from './auth'; +import { splitIntoParagraphChunks } from '../../utils/message-splitting'; +import type { LinearWebhookPayload, LinearIssueData } from './types'; +import { isIssueData } from './types'; + +let cachedLog: ReturnType | undefined; +function getLog(): ReturnType { + if (!cachedLog) cachedLog = createLogger('adapter.linear'); + return cachedLog; +} + +const MAX_LENGTH = 65000; + +const PRIORITY_LABELS: Record = { + 0: 'No priority', + 1: 'Urgent', + 2: 'High', + 3: 'Medium', + 4: 'Low', +}; + +const CONVERSATION_ID_PREFIX = 'linear/'; + +interface ParsedConversationId { + team: string; + identifier: string; +} + +/** + * Parse a `linear/{team}/{identifier}` conversationId. Returns undefined for + * conversation IDs from other platforms or malformed strings. + */ +function parseConversationId(conversationId: string): ParsedConversationId | undefined { + if (!conversationId.startsWith(CONVERSATION_ID_PREFIX)) return undefined; + const rest = conversationId.slice(CONVERSATION_ID_PREFIX.length); + const slash = rest.indexOf('/'); + if (slash <= 0 || slash === rest.length - 1) return undefined; + return { team: rest.slice(0, slash), identifier: rest.slice(slash + 1) }; +} + +export class LinearAdapter implements IPlatformAdapter { + private readonly linearClient: LinearClient; + private readonly webhookSecret: string; + private readonly lockManager: ConversationLockManager; + private readonly targetAssignee: string; + private readonly defaultWorkflow: string; + private readonly teamCodebaseMap: ReadonlyMap; + + constructor( + apiKey: string, + webhookSecret: string, + lockManager: ConversationLockManager, + config: LinearConfig = {} + ) { + this.linearClient = new LinearClient({ apiKey }); + this.webhookSecret = webhookSecret; + this.lockManager = lockManager; + this.targetAssignee = config.assignee ?? 'archon'; + this.defaultWorkflow = config.workflow ?? 'implement'; + this.teamCodebaseMap = new Map(Object.entries(config.mappings ?? {})); + } + + // ─── IPlatformAdapter ─────────────────────────────────────────────────────── + + async sendMessage( + conversationId: string, + message: string, + _metadata?: MessageMetadata + ): Promise { + const issueId = await this.resolveIssueUuid(conversationId); + const chunks = splitIntoParagraphChunks(message, MAX_LENGTH); + for (const chunk of chunks) { + await this.linearClient.createComment({ issueId, body: chunk }); + } + } + + async ensureThread(originalConversationId: string, _messageContext?: unknown): Promise { + return originalConversationId; + } + + getStreamingMode(): 'stream' | 'batch' { + return 'batch'; + } + + getPlatformType(): string { + return 'linear'; + } + + async start(): Promise { + getLog().info('linear.adapter_started'); + } + + stop(): void { + getLog().info('linear.adapter_stopped'); + } + + // ─── Webhook Processing ───────────────────────────────────────────────────── + + async handleWebhook(payload: string, signature: string): Promise { + if (!verifyLinearSignature(payload, signature, this.webhookSecret)) { + getLog().error('linear.signature_verification_failed'); + return; + } + + let event: LinearWebhookPayload; + try { + event = JSON.parse(payload) as LinearWebhookPayload; + } catch { + getLog().error('linear.payload_parse_failed'); + return; + } + + // Only process issue update events with a state transition. + if (event.action !== 'update' || !isIssueData(event) || !event.updatedFrom?.stateId) { + return; + } + + const issue = event.data; + + // Only trigger when state transitions to "started" (In Progress). + if (issue.state.type !== 'started') return; + + // Linear's webhook payload reliably includes `name` (full name) but + // `displayName` (short handle) is optional and often absent. Match + // against either so config can point at whichever field the user sees. + const assigneeCandidates = [issue.assignee?.displayName, issue.assignee?.name]; + if (!assigneeCandidates.some(n => isTargetAssignee(n, this.targetAssignee))) return; + + getLog().info( + { + issueId: issue.identifier, + team: issue.team.key, + assignee: issue.assignee?.displayName ?? issue.assignee?.name, + }, + 'linear.issue_triggered' + ); + + const conversationId = `${CONVERSATION_ID_PREFIX}${issue.team.key}/${issue.identifier}`; + + await this.lockManager.acquireLock(conversationId, async () => { + await this.processIssue(conversationId, issue); + }); + } + + // ─── Internal ─────────────────────────────────────────────────────────────── + + private async processIssue(conversationId: string, issue: LinearIssueData): Promise { + const codebaseName = this.teamCodebaseMap.get(issue.team.key); + if (!codebaseName) { + getLog().warn( + { teamKey: issue.team.key, issueId: issue.identifier }, + 'linear.no_team_mapping' + ); + await this.sendMessage( + conversationId, + `No codebase mapping found for team **${issue.team.key}**. ` + + 'Configure `linear.mappings` in `~/.archon/config.yaml`.' + ); + return; + } + + const codebase = await codebaseDb.findCodebaseByName(codebaseName); + if (!codebase) { + getLog().error( + { codebaseName, teamKey: issue.team.key, issueId: issue.identifier }, + 'linear.codebase_not_found' + ); + await this.sendMessage( + conversationId, + `Codebase **${codebaseName}** not found. Register it first via the web UI or \`/clone\`.` + ); + return; + } + + // Link conversation → codebase BEFORE handing off to the orchestrator, + // mirroring the GitHub adapter pattern (forge/github/adapter.ts). + const conversation = await db.getOrCreateConversation('linear', conversationId); + if (conversation.codebase_id !== codebase.id) { + await db.updateConversation(conversation.id, { codebase_id: codebase.id }); + } + + const isolationHints: IsolationHints = { + workflowType: 'issue', + workflowId: issue.identifier, + }; + + const issueContext = this.buildIssueContext(issue); + const finalMessage = `/workflow run ${this.defaultWorkflow}`; + + try { + await handleMessage(this, conversationId, finalMessage, { + issueContext, + isolationHints, + }); + } catch (error) { + getLog().error( + { err: error as Error, issueId: issue.identifier, workflow: this.defaultWorkflow }, + 'linear.handle_message_failed' + ); + await this.sendMessage( + conversationId, + `Failed to dispatch workflow **${this.defaultWorkflow}**: ${(error as Error).message}` + ); + } + } + + private buildIssueContext(issue: LinearIssueData): string { + const priority = PRIORITY_LABELS[issue.priority] ?? `Priority ${issue.priority}`; + const labels = issue.labels.map(l => l.name).join(', ') || 'none'; + const description = issue.description?.trim() || '(No description provided)'; + + return [ + `## Linear Issue: ${issue.identifier}`, + '', + `**Title:** ${issue.title}`, + `**Team:** ${issue.team.name}`, + `**Priority:** ${priority}`, + `**Labels:** ${labels}`, + '', + '### Description', + '', + description, + '', + '---', + '', + 'Implement the changes described in this Linear issue.', + ].join('\n'); + } + + /** + * Resolve a Linear issue UUID from a conversationId. Linear's `issue` query + * accepts either the UUID or the human identifier (e.g. "ENG-123"), so we + * derive it on demand from the conversationId rather than caching in memory + * (which would break across restarts). + */ + private async resolveIssueUuid(conversationId: string): Promise { + const parsed = parseConversationId(conversationId); + if (!parsed) { + throw new Error(`Invalid Linear conversationId: ${conversationId}`); + } + const issue = await this.linearClient.issue(parsed.identifier); + return issue.id; + } +} diff --git a/packages/adapters/src/pm/linear/auth.ts b/packages/adapters/src/pm/linear/auth.ts new file mode 100644 index 0000000000..21d9bc240d --- /dev/null +++ b/packages/adapters/src/pm/linear/auth.ts @@ -0,0 +1,34 @@ +/** + * Linear webhook authentication and authorization utilities. + * Signature verification follows HMAC-SHA256 pattern (same as GitHub adapter). + */ +import { createHmac, timingSafeEqual } from 'crypto'; + +/** + * Verify a Linear webhook signature (HMAC-SHA256). + * Linear sends the signature in the `linear-signature` header as a hex digest. + */ +export function verifyLinearSignature(payload: string, signature: string, secret: string): boolean { + const hmac = createHmac('sha256', secret); + const digest = hmac.update(payload).digest('hex'); + + const digestBuffer = Buffer.from(digest); + const signatureBuffer = Buffer.from(signature); + + if (digestBuffer.length !== signatureBuffer.length) { + return false; + } + + return timingSafeEqual(digestBuffer, signatureBuffer); +} + +/** + * Check if the issue assignee matches the configured target (case-insensitive). + */ +export function isTargetAssignee( + assigneeName: string | undefined, + configuredAssignee: string +): boolean { + if (!assigneeName) return false; + return assigneeName.toLowerCase() === configuredAssignee.toLowerCase(); +} diff --git a/packages/adapters/src/pm/linear/index.ts b/packages/adapters/src/pm/linear/index.ts new file mode 100644 index 0000000000..fb9cce1fa8 --- /dev/null +++ b/packages/adapters/src/pm/linear/index.ts @@ -0,0 +1 @@ +export { LinearAdapter } from './adapter'; diff --git a/packages/adapters/src/pm/linear/types.ts b/packages/adapters/src/pm/linear/types.ts new file mode 100644 index 0000000000..21c0118661 --- /dev/null +++ b/packages/adapters/src/pm/linear/types.ts @@ -0,0 +1,84 @@ +/** + * Linear webhook event type definitions. + * Based on Linear's webhook API: https://linear.app/docs/webhooks + */ + +export interface LinearWebhookPayload { + action: 'create' | 'update' | 'remove'; + type: 'Issue' | 'Comment' | (string & {}); + data: LinearIssueData | LinearCommentData; + /** Previous values for updated fields (only present on 'update' actions) */ + updatedFrom?: Record; + createdAt: string; + organizationId: string; +} + +export interface LinearIssueState { + id: string; + name: string; + /** Linear state type — 'started' corresponds to "In Progress" */ + type: string; +} + +export interface LinearAssignee { + id: string; + name: string; + displayName: string; +} + +export interface LinearTeam { + id: string; + key: string; + name: string; +} + +export interface LinearLabel { + id: string; + name: string; +} + +export interface LinearIssueData { + id: string; + /** Human-readable identifier, e.g. "ENG-123" */ + identifier: string; + title: string; + description: string | null; + priority: number; + state: LinearIssueState; + assignee?: LinearAssignee; + team: LinearTeam; + labels: LinearLabel[]; + url: string; + project?: { id: string; name: string }; +} + +export interface LinearCommentData { + id: string; + body: string; + issueId: string; + userId: string; + issue: { id: string; identifier: string }; +} + +/** + * Type guard: check if webhook data is an issue payload. Performs a structural + * check so a malformed payload with `type: 'Issue'` but the wrong shape does + * not slip past discriminator checks downstream. + */ +export function isIssueData( + payload: LinearWebhookPayload +): payload is LinearWebhookPayload & { data: LinearIssueData } { + if (payload.type !== 'Issue') return false; + const data = payload.data as Partial | null | undefined; + if (!data || typeof data !== 'object') return false; + return ( + typeof data.id === 'string' && + typeof data.identifier === 'string' && + typeof data.state === 'object' && + data.state !== null && + typeof data.state.type === 'string' && + typeof data.team === 'object' && + data.team !== null && + typeof data.team.key === 'string' + ); +} diff --git a/packages/core/src/config/config-types.ts b/packages/core/src/config/config-types.ts index 6e3611c5b2..0665ac8e3f 100644 --- a/packages/core/src/config/config-types.ts +++ b/packages/core/src/config/config-types.ts @@ -59,6 +59,20 @@ export type AssistantDefaults = ProviderDefaultsMap & { codex: CodexProviderDefaults; }; +/** + * Linear webhook integration configuration. + * When configured, issues assigned to the target user that transition to + * "In Progress" automatically trigger a workflow execution. + */ +export interface LinearConfig { + /** Linear assignee display name to match (case-insensitive). @default 'archon' */ + assignee?: string; + /** Workflow name to trigger when an issue is activated. @default 'implement' */ + workflow?: string; + /** Maps Linear team keys to registered Archon codebase names. */ + mappings?: Record; +} + export interface GlobalConfig { /** * Bot display name (shown in messages) @@ -113,6 +127,12 @@ export interface GlobalConfig { */ maxConversations?: number; }; + + /** + * Linear webhook integration settings. + * Requires LINEAR_API_KEY and LINEAR_WEBHOOK_SECRET env vars. + */ + linear?: LinearConfig; } /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8c5e928a98..40184cfc0e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -83,6 +83,7 @@ export { cloneRepository, registerRepository, type RegisterResult } from './hand // ============================================================================= export { type GlobalConfig, + type LinearConfig, type RepoConfig, type MergedConfig, type SafeConfig, diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index d5eb9397b3..60bb7bb72e 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -705,12 +705,17 @@ export async function handleMessage( await platform.sendMessage(conversationId, result.message); if (result.workflow) { + // Prefer explicit args from the slash command. When absent (e.g. an + // adapter synthesised `/workflow run ` with no inline args), + // fall back to issueContext so webhook-triggered workflows still + // receive the rich trigger context as their `$ARGUMENTS`. + const workflowArgs = result.workflow.args || issueContext || message; await handleWorkflowRunCommand( platform, conversationId, conversation, result.workflow.definition, - result.workflow.args ?? message, + workflowArgs, isolationHints ); } diff --git a/packages/providers/src/claude/provider.ts b/packages/providers/src/claude/provider.ts index 26935bf373..aa7dab822a 100644 --- a/packages/providers/src/claude/provider.ts +++ b/packages/providers/src/claude/provider.ts @@ -379,8 +379,15 @@ async function applyNodeConfig( nodeConfig.hooks as Record ); if (Object.keys(builtHooks).length > 0) { + // Some callers (e.g. the warnings-extraction path in sendQuery) pass a + // throwaway options object with no `hooks` set. Initialize defensively so + // the merge below doesn't crash with "undefined is not an object" when + // a node declares `hooks:` in YAML. + if (!options.hooks) { + options.hooks = {} as SDKHooksMap; + } // Merge with existing hooks (PostToolUse capture hook) - const existingHooks = options.hooks as SDKHooksMap | undefined; + const existingHooks = options.hooks as SDKHooksMap; for (const [event, matchers] of Object.entries(builtHooks)) { if (!matchers) continue; const existing = existingHooks?.[event] as HookCallbackMatcher[] | undefined; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index aa0940cd8d..c36b498ce6 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -60,7 +60,13 @@ registerCommunityProviders(); import { OpenAPIHono } from '@hono/zod-openapi'; import { validationErrorHook } from './routes/openapi-defaults'; -import { TelegramAdapter, GitHubAdapter, DiscordAdapter, SlackAdapter } from '@archon/adapters'; +import { + TelegramAdapter, + GitHubAdapter, + DiscordAdapter, + SlackAdapter, + LinearAdapter, +} from '@archon/adapters'; import { GiteaAdapter } from '@archon/adapters/community/forge/gitea'; import { GitLabAdapter } from '@archon/adapters/community/forge/gitlab'; import { WebAdapter } from './adapters/web'; @@ -76,6 +82,7 @@ import { startCleanupScheduler, stopCleanupScheduler, loadConfig, + loadGlobalConfig, logConfig, getPort, } from '@archon/core'; @@ -263,6 +270,7 @@ export async function startServer(opts: ServerOptions = {}): Promise { let gitlab: GitLabAdapter | null = null; let discord: DiscordAdapter | null = null; let slack: SlackAdapter | null = null; + let linear: LinearAdapter | null = null; if (!opts.skipPlatformAdapters) { // Check that at least one platform is configured @@ -273,8 +281,9 @@ export async function startServer(opts: ServerOptions = {}): Promise { process.env.GITEA_URL && process.env.GITEA_TOKEN && process.env.GITEA_WEBHOOK_SECRET ); const hasGitLab = Boolean(process.env.GITLAB_TOKEN && process.env.GITLAB_WEBHOOK_SECRET); + const hasLinear = Boolean(process.env.LINEAR_API_KEY && process.env.LINEAR_WEBHOOK_SECRET); - if (!hasTelegram && !hasDiscord && !hasGitHub && !hasGitea && !hasGitLab) { + if (!hasTelegram && !hasDiscord && !hasGitHub && !hasGitea && !hasGitLab && !hasLinear) { getLog().warn('no_platform_adapters_configured'); } @@ -441,6 +450,20 @@ export async function startServer(opts: ServerOptions = {}): Promise { } else { getLog().info('slack_adapter_skipped'); } + + // Initialize Linear adapter (conditional) + if (process.env.LINEAR_API_KEY && process.env.LINEAR_WEBHOOK_SECRET) { + const globalConfig = await loadGlobalConfig(); + linear = new LinearAdapter( + process.env.LINEAR_API_KEY, + process.env.LINEAR_WEBHOOK_SECRET, + lockManager, + globalConfig.linear ?? {} + ); + await linear.start(); + } else { + getLog().info('linear.adapter_skipped'); + } } else { getLog().info('platform_adapters_skipped'); } @@ -543,6 +566,32 @@ export async function startServer(opts: ServerOptions = {}): Promise { getLog().info('gitlab_webhook_registered'); } + // Linear webhook endpoint + if (linear) { + app.post('/webhooks/linear', async c => { + try { + const signature = c.req.header('linear-signature'); + if (!signature) { + return c.json({ error: 'Missing signature header' }, 400); + } + + // CRITICAL: Use c.req.text() for raw body (signature verification) + const payload = await c.req.text(); + + // Process async (fire-and-forget for fast webhook response) + linear.handleWebhook(payload, signature).catch((error: unknown) => { + getLog().error({ err: error }, 'linear.webhook_processing_error'); + }); + + return c.text('OK', 200); + } catch (error) { + getLog().error({ err: error }, 'linear.webhook_endpoint_error'); + return c.json({ error: 'Internal server error' }, 500); + } + }); + getLog().info('linear.webhook_registered'); + } + // Health check endpoints app.get('/health', c => { return c.json({ status: 'ok' });