@mastra/factory 0.5.1-alpha.0 → 0.6.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/auth.d.ts +2 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +2 -0
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/intake.d.ts +1 -0
  7. package/dist/capabilities/intake.d.ts.map +1 -1
  8. package/dist/capabilities/version-control.d.ts +3 -0
  9. package/dist/capabilities/version-control.d.ts.map +1 -1
  10. package/dist/factory.d.ts +10 -0
  11. package/dist/factory.d.ts.map +1 -1
  12. package/dist/factory.js +15 -2
  13. package/dist/factory.js.map +1 -1
  14. package/dist/integrations/base.d.ts +6 -0
  15. package/dist/integrations/base.d.ts.map +1 -1
  16. package/dist/integrations/github/integration.d.ts +14 -1
  17. package/dist/integrations/github/integration.d.ts.map +1 -1
  18. package/dist/integrations/github/integration.js +61 -3
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-reconciler.d.ts +24 -0
  21. package/dist/integrations/github/issue-reconciler.d.ts.map +1 -0
  22. package/dist/integrations/github/issue-reconciler.js +104 -0
  23. package/dist/integrations/github/issue-reconciler.js.map +1 -0
  24. package/dist/integrations/github/reconcile-worker.d.ts +8 -0
  25. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  26. package/dist/integrations/github/reconcile-worker.js +38 -1
  27. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  28. package/dist/integrations/github/routes.d.ts.map +1 -1
  29. package/dist/integrations/github/routes.js +65 -52
  30. package/dist/integrations/github/routes.js.map +1 -1
  31. package/dist/integrations/github/rules.d.ts +37 -2
  32. package/dist/integrations/github/rules.d.ts.map +1 -1
  33. package/dist/integrations/github/rules.js +150 -14
  34. package/dist/integrations/github/rules.js.map +1 -1
  35. package/dist/integrations/github/sandbox-release.d.ts +16 -1
  36. package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
  37. package/dist/integrations/github/sandbox-release.js +42 -1
  38. package/dist/integrations/github/sandbox-release.js.map +1 -1
  39. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  40. package/dist/integrations/github/sandbox.js +74 -8
  41. package/dist/integrations/github/sandbox.js.map +1 -1
  42. package/dist/integrations/issue-reconcile-worker.d.ts +19 -0
  43. package/dist/integrations/issue-reconcile-worker.d.ts.map +1 -0
  44. package/dist/integrations/issue-reconcile-worker.js +98 -0
  45. package/dist/integrations/issue-reconcile-worker.js.map +1 -0
  46. package/dist/integrations/issue-reconciler.d.ts +47 -0
  47. package/dist/integrations/issue-reconciler.d.ts.map +1 -0
  48. package/dist/integrations/issue-reconciler.js +104 -0
  49. package/dist/integrations/issue-reconciler.js.map +1 -0
  50. package/dist/integrations/linear/integration.d.ts +4 -0
  51. package/dist/integrations/linear/integration.d.ts.map +1 -1
  52. package/dist/integrations/linear/integration.js +19 -2
  53. package/dist/integrations/linear/integration.js.map +1 -1
  54. package/dist/integrations/linear/issue-reconciler.d.ts +6 -0
  55. package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -0
  56. package/dist/integrations/linear/issue-reconciler.js +64 -0
  57. package/dist/integrations/linear/issue-reconciler.js.map +1 -0
  58. package/dist/integrations/linear/routes.d.ts.map +1 -1
  59. package/dist/integrations/linear/routes.js +1 -0
  60. package/dist/integrations/linear/routes.js.map +1 -1
  61. package/dist/integrations/linear/rules.d.ts +2 -0
  62. package/dist/integrations/linear/rules.d.ts.map +1 -1
  63. package/dist/integrations/linear/rules.js +5 -2
  64. package/dist/integrations/linear/rules.js.map +1 -1
  65. package/dist/integrations/platform/github/event-worker.d.ts +7 -0
  66. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  67. package/dist/integrations/platform/github/event-worker.js +24 -0
  68. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  69. package/dist/integrations/platform/github/integration.d.ts +13 -3
  70. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  71. package/dist/integrations/platform/github/integration.js +42 -1
  72. package/dist/integrations/platform/github/integration.js.map +1 -1
  73. package/dist/integrations/platform/linear/event-worker.d.ts +105 -0
  74. package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -0
  75. package/dist/integrations/platform/linear/event-worker.js +380 -0
  76. package/dist/integrations/platform/linear/event-worker.js.map +1 -0
  77. package/dist/integrations/platform/linear/integration.d.ts +9 -0
  78. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  79. package/dist/integrations/platform/linear/integration.js +37 -1
  80. package/dist/integrations/platform/linear/integration.js.map +1 -1
  81. package/dist/integrations/slack/integration.d.ts.map +1 -1
  82. package/dist/integrations/slack/integration.js +3 -2
  83. package/dist/integrations/slack/integration.js.map +1 -1
  84. package/dist/integrations/slack/slack.d.ts +37 -110
  85. package/dist/integrations/slack/slack.d.ts.map +1 -1
  86. package/dist/integrations/slack/slack.js +49 -44
  87. package/dist/integrations/slack/slack.js.map +1 -1
  88. package/dist/routes/config.d.ts +25 -0
  89. package/dist/routes/config.d.ts.map +1 -1
  90. package/dist/routes/config.js +63 -1
  91. package/dist/routes/config.js.map +1 -1
  92. package/dist/routes/fs.d.ts +16 -2
  93. package/dist/routes/fs.d.ts.map +1 -1
  94. package/dist/routes/fs.js +47 -5
  95. package/dist/routes/fs.js.map +1 -1
  96. package/dist/routes/surface.d.ts +13 -1
  97. package/dist/routes/surface.d.ts.map +1 -1
  98. package/dist/routes/surface.js +20 -9
  99. package/dist/routes/surface.js.map +1 -1
  100. package/dist/rules/binding-context.d.ts +28 -1
  101. package/dist/rules/binding-context.d.ts.map +1 -1
  102. package/dist/rules/binding-context.js +66 -1
  103. package/dist/rules/binding-context.js.map +1 -1
  104. package/dist/rules/defaults.d.ts.map +1 -1
  105. package/dist/rules/defaults.js +71 -5
  106. package/dist/rules/defaults.js.map +1 -1
  107. package/dist/rules/start-coordinator.d.ts.map +1 -1
  108. package/dist/rules/start-coordinator.js +7 -26
  109. package/dist/rules/start-coordinator.js.map +1 -1
  110. package/dist/rules/tools.d.ts +2 -0
  111. package/dist/rules/tools.d.ts.map +1 -1
  112. package/dist/rules/tools.js +15 -6
  113. package/dist/rules/tools.js.map +1 -1
  114. package/dist/rules/types.d.ts +17 -2
  115. package/dist/rules/types.d.ts.map +1 -1
  116. package/dist/rules/types.js +2 -1
  117. package/dist/rules/types.js.map +1 -1
  118. package/dist/session/factory-session.d.ts +116 -0
  119. package/dist/session/factory-session.d.ts.map +1 -0
  120. package/dist/session/factory-session.js +162 -0
  121. package/dist/session/factory-session.js.map +1 -0
  122. package/dist/session/filesystem-capture.d.ts +26 -0
  123. package/dist/session/filesystem-capture.d.ts.map +1 -0
  124. package/dist/session/filesystem-capture.js +75 -0
  125. package/dist/session/filesystem-capture.js.map +1 -0
  126. package/dist/storage/domains/audit/domain.d.ts +9 -2
  127. package/dist/storage/domains/audit/domain.d.ts.map +1 -1
  128. package/dist/storage/domains/audit/domain.js +106 -4
  129. package/dist/storage/domains/audit/domain.js.map +1 -1
  130. package/dist/storage/domains/filesystem/base.d.ts +26 -0
  131. package/dist/storage/domains/filesystem/base.d.ts.map +1 -0
  132. package/dist/storage/domains/filesystem/base.js +74 -0
  133. package/dist/storage/domains/filesystem/base.js.map +1 -0
  134. package/dist/storage/domains/projects/base.d.ts +1 -0
  135. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  136. package/dist/storage/domains/projects/base.js +3 -0
  137. package/dist/storage/domains/projects/base.js.map +1 -1
  138. package/dist/storage/domains/source-control/base.d.ts +2 -0
  139. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  140. package/dist/storage/domains/source-control/base.js +6 -0
  141. package/dist/storage/domains/source-control/base.js.map +1 -1
  142. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  143. package/dist/storage/domains/source-control/inmemory.js +3 -0
  144. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  145. package/dist/storage/domains/work-items/base.d.ts +11 -0
  146. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  147. package/dist/storage/domains/work-items/base.js +28 -0
  148. package/dist/storage/domains/work-items/base.js.map +1 -1
  149. package/factory-skills/factory-triage/SKILL.md +45 -12
  150. package/package.json +5 -5
  151. package/dist/integrations/github/factory-session.d.ts +0 -12
  152. package/dist/integrations/github/factory-session.d.ts.map +0 -1
  153. package/dist/integrations/github/factory-session.js +0 -37
  154. package/dist/integrations/github/factory-session.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/linear/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Linear intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the WorkOS auth gate.\n * Mirrors the GitHub module: every route re-resolves the authenticated user\n * from the request cookie and scopes all rows by the caller's WorkOS org, so an\n * org can only ever see its own Linear connection and issues.\n *\n * When the feature is disabled (`isLinearFeatureEnabled()` false),\n * `buildLinearRoutes` returns only `GET /web/linear/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Linear UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport type { LinearIntegration } from './integration.js';\nimport { LinearReauthRequiredError } from './integration.js';\nimport type { LinearRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Linear feature gate, mirroring the\n * GitHub diagnostics shape. Only booleans — never values.\n */\nexport interface LinearFeatureDiagnostics {\n linearAppConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountLinearRoutesOptions {\n /**\n * The integration instance providing OAuth + GraphQL access. Required for\n * everything beyond the disabled `status` route.\n */\n linear?: LinearIntegration;\n /** Host auth seam. Linear connections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Absolute base URL of the web server (e.g. `http://localhost:4111`), used to\n * build the OAuth redirect URI when one isn't explicitly configured.\n */\n baseUrl?: string;\n /** Explicit OAuth callback URI; defaults to `<baseUrl>/auth/linear/callback`. */\n redirectUri?: string;\n /**\n * Shared OAuth `state` signer (created once per boot by the factory).\n * Required for the connect/callback flow; when absent, only the disabled\n * `status` route is served.\n */\n stateSigner?: StateSigner;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n ingestFactoryIssues?: (input: LinearRulesIngress) => Promise<unknown>;\n}\n\n/**\n * Resolve the org-scoped tenant for a Linear request. The connection is\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the GitHub routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Linear intake requires an organization. Personal accounts cannot connect Linear.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Linear pagination cursor from the query string. Cursors\n * are server-issued (`pageInfo.endCursor`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Linear.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 512 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Map a Linear read failure to the API response for the SPA. */\nfunction linearFetchError(c: RouteContext, err: unknown) {\n if (err instanceof LinearReauthRequiredError || (err as { status?: number }).status === 401) {\n return c.json({ error: 'linear_reauth_required', message: new LinearReauthRequiredError().message }, 409);\n }\n return c.json({ error: 'linear_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Linear routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildLinearRoutes(options: MountLinearRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { linear, auth, stateSigner, intake } = options;\n const enabled = Boolean(linear) && auth.enabled();\n const diagnostics = (): LinearFeatureDiagnostics => ({\n linearAppConfigured: Boolean(linear),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/linear/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !linear || !stateSigner) {\n return c.json({\n enabled: false,\n connected: false,\n workspace: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n organizationRequired: true,\n connected: false,\n workspace: null,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n const connection = await linear.loadConnection(tenant.orgId);\n return c.json({\n enabled: true,\n connected: Boolean(connection),\n workspace: connection ? { name: connection.workspaceName, urlKey: connection.workspaceUrlKey } : null,\n reason: connection ? 'ready' : 'not_connected',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or a state signer the connect/callback\n // flow cannot talk to Linear or bind the OAuth round-trip to a tenant —\n // serve only the disabled `status` route (mirrors the feature gate).\n if (!enabled || !linear || !stateSigner || !intake) {\n return routes;\n }\n\n const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? '').replace(/\\/$/, '')}/auth/linear/callback`;\n\n // ── Connect: send the user to Linear's OAuth consent screen ─────────────\n routes.push(\n registerApiRoute('/auth/linear/connect', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const state = stateSigner.sign(resolved.tenant.orgId, resolved.tenant.userId);\n return c.redirect(linear.buildAuthorizeUrl(state, redirectUri));\n },\n }),\n );\n\n // ── Callback: exchange the code, persist the connection for the org ─────\n routes.push(\n registerApiRoute('/auth/linear/callback', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const { orgId, userId } = resolved.tenant;\n\n // CSRF / cross-tenant linking protection: the signed state must belong\n // to the same logged-in user *and* their current org.\n const stateTenant = stateSigner.verify(c.req.query('state'));\n if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {\n console.warn('[Linear] OAuth callback rejected: state/tenant mismatch.');\n return c.redirect('/?linear=error');\n }\n\n const code = c.req.query('code');\n if (!code) {\n // User denied consent (or Linear returned an error).\n return c.redirect('/?linear=error');\n }\n\n try {\n const tokens = await linear.exchangeOAuthCode(code, redirectUri);\n const workspace = await linear.fetchWorkspace(tokens.accessToken);\n await linear.upsertConnection({\n orgId,\n userId,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n workspaceName: workspace.name,\n workspaceUrlKey: workspace.urlKey,\n });\n } catch (error) {\n console.warn(`[Linear] OAuth callback failed to persist connection for org ${orgId}.`, error);\n return c.redirect('/?linear=error');\n }\n\n return c.redirect('/?linear=connected');\n },\n }),\n );\n\n // ── List the workspace's projects (Settings intake-source picker) ───────\n routes.push(\n registerApiRoute('/web/linear/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to list Linear projects.' }, 409);\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const projects = await linear.listProjects(accessToken);\n return c.json({ projects });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the workspace's active issues (cursor-paged) ───────────────────\n // Respects the caller's intake config: disabled Linear intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/linear/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to see intake issues.' }, 409);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n integrationIds: ['linear'],\n });\n const selection = config.linear!;\n if (!selection.enabled) {\n return c.json({ error: 'linear_intake_disabled', message: 'Linear intake is turned off in Settings.' }, 404);\n }\n\n // No projects selected means nothing is synced — don't fan out to Linear.\n const projectIds = selection.sourceIds ?? [];\n if (projectIds.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const { issues, nextCursor } = await linear.intake.listIssues({\n connection: { type: 'oauth', accessToken },\n sourceIds: projectIds,\n cursor: after,\n });\n const issuePayload = issues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n team: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n }));\n if (factoryProjectId && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/linear/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Linear intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the WorkOS auth gate.\n * Mirrors the GitHub module: every route re-resolves the authenticated user\n * from the request cookie and scopes all rows by the caller's WorkOS org, so an\n * org can only ever see its own Linear connection and issues.\n *\n * When the feature is disabled (`isLinearFeatureEnabled()` false),\n * `buildLinearRoutes` returns only `GET /web/linear/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Linear UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport type { LinearIntegration } from './integration.js';\nimport { LinearReauthRequiredError } from './integration.js';\nimport type { LinearRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Linear feature gate, mirroring the\n * GitHub diagnostics shape. Only booleans — never values.\n */\nexport interface LinearFeatureDiagnostics {\n linearAppConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountLinearRoutesOptions {\n /**\n * The integration instance providing OAuth + GraphQL access. Required for\n * everything beyond the disabled `status` route.\n */\n linear?: LinearIntegration;\n /** Host auth seam. Linear connections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Absolute base URL of the web server (e.g. `http://localhost:4111`), used to\n * build the OAuth redirect URI when one isn't explicitly configured.\n */\n baseUrl?: string;\n /** Explicit OAuth callback URI; defaults to `<baseUrl>/auth/linear/callback`. */\n redirectUri?: string;\n /**\n * Shared OAuth `state` signer (created once per boot by the factory).\n * Required for the connect/callback flow; when absent, only the disabled\n * `status` route is served.\n */\n stateSigner?: StateSigner;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n ingestFactoryIssues?: (input: LinearRulesIngress) => Promise<unknown>;\n}\n\n/**\n * Resolve the org-scoped tenant for a Linear request. The connection is\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the GitHub routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Linear intake requires an organization. Personal accounts cannot connect Linear.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Linear pagination cursor from the query string. Cursors\n * are server-issued (`pageInfo.endCursor`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Linear.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 512 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Map a Linear read failure to the API response for the SPA. */\nfunction linearFetchError(c: RouteContext, err: unknown) {\n if (err instanceof LinearReauthRequiredError || (err as { status?: number }).status === 401) {\n return c.json({ error: 'linear_reauth_required', message: new LinearReauthRequiredError().message }, 409);\n }\n return c.json({ error: 'linear_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Linear routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildLinearRoutes(options: MountLinearRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { linear, auth, stateSigner, intake } = options;\n const enabled = Boolean(linear) && auth.enabled();\n const diagnostics = (): LinearFeatureDiagnostics => ({\n linearAppConfigured: Boolean(linear),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/linear/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !linear || !stateSigner) {\n return c.json({\n enabled: false,\n connected: false,\n workspace: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n organizationRequired: true,\n connected: false,\n workspace: null,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n const connection = await linear.loadConnection(tenant.orgId);\n return c.json({\n enabled: true,\n connected: Boolean(connection),\n workspace: connection ? { name: connection.workspaceName, urlKey: connection.workspaceUrlKey } : null,\n reason: connection ? 'ready' : 'not_connected',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or a state signer the connect/callback\n // flow cannot talk to Linear or bind the OAuth round-trip to a tenant —\n // serve only the disabled `status` route (mirrors the feature gate).\n if (!enabled || !linear || !stateSigner || !intake) {\n return routes;\n }\n\n const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? '').replace(/\\/$/, '')}/auth/linear/callback`;\n\n // ── Connect: send the user to Linear's OAuth consent screen ─────────────\n routes.push(\n registerApiRoute('/auth/linear/connect', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const state = stateSigner.sign(resolved.tenant.orgId, resolved.tenant.userId);\n return c.redirect(linear.buildAuthorizeUrl(state, redirectUri));\n },\n }),\n );\n\n // ── Callback: exchange the code, persist the connection for the org ─────\n routes.push(\n registerApiRoute('/auth/linear/callback', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const { orgId, userId } = resolved.tenant;\n\n // CSRF / cross-tenant linking protection: the signed state must belong\n // to the same logged-in user *and* their current org.\n const stateTenant = stateSigner.verify(c.req.query('state'));\n if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {\n console.warn('[Linear] OAuth callback rejected: state/tenant mismatch.');\n return c.redirect('/?linear=error');\n }\n\n const code = c.req.query('code');\n if (!code) {\n // User denied consent (or Linear returned an error).\n return c.redirect('/?linear=error');\n }\n\n try {\n const tokens = await linear.exchangeOAuthCode(code, redirectUri);\n const workspace = await linear.fetchWorkspace(tokens.accessToken);\n await linear.upsertConnection({\n orgId,\n userId,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n workspaceName: workspace.name,\n workspaceUrlKey: workspace.urlKey,\n });\n } catch (error) {\n console.warn(`[Linear] OAuth callback failed to persist connection for org ${orgId}.`, error);\n return c.redirect('/?linear=error');\n }\n\n return c.redirect('/?linear=connected');\n },\n }),\n );\n\n // ── List the workspace's projects (Settings intake-source picker) ───────\n routes.push(\n registerApiRoute('/web/linear/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to list Linear projects.' }, 409);\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const projects = await linear.listProjects(accessToken);\n return c.json({ projects });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the workspace's active issues (cursor-paged) ───────────────────\n // Respects the caller's intake config: disabled Linear intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/linear/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to see intake issues.' }, 409);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n integrationIds: ['linear'],\n });\n const selection = config.linear!;\n if (!selection.enabled) {\n return c.json({ error: 'linear_intake_disabled', message: 'Linear intake is turned off in Settings.' }, 404);\n }\n\n // No projects selected means nothing is synced — don't fan out to Linear.\n const projectIds = selection.sourceIds ?? [];\n if (projectIds.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const { issues, nextCursor } = await linear.intake.listIssues({\n connection: { type: 'oauth', accessToken },\n sourceIds: projectIds,\n cursor: after,\n });\n const issuePayload = issues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n creator: issue.author,\n team: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n }));\n if (factoryProjectId && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
@@ -11,6 +11,8 @@ export interface LinearIssueIngress {
11
11
  stateType: string;
12
12
  priorityLabel: string;
13
13
  assignee: string | null;
14
+ /** Display name of the Linear user who created the issue, when available. */
15
+ creator: string | null;
14
16
  team: string | null;
15
17
  labels: string[];
16
18
  createdAt: string;
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiD,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAExG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkBrD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,KAAK,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,qBAAa,WAAW;;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAuF9F;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,CAAC,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAQ/D"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiD,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAExG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkBrD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,KAAK,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,qBAAa,WAAW;;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CA+F9F;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,CAAC,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAQ/D"}
@@ -51,6 +51,9 @@ var LinearRules = class {
51
51
  type: "human",
52
52
  id: input.userId
53
53
  };
54
+ const isClosed = issue.stateType === "completed" || issue.stateType === "canceled";
55
+ if (isClosed && !relatedItem) return "missing";
56
+ const event = isClosed ? "issueClosed" : "issueObserved";
54
57
  const context = {
55
58
  tenant: {
56
59
  orgId: input.orgId,
@@ -61,7 +64,7 @@ var LinearRules = class {
61
64
  type: "linear",
62
65
  id: ingressId
63
66
  },
64
- cause: "linear.issueObserved",
67
+ cause: `linear.${event}`,
65
68
  causalChain: [],
66
69
  ruleSetVersion: this.options.rules.version,
67
70
  ...relatedItem ? {
@@ -77,7 +80,7 @@ var LinearRules = class {
77
80
  board: "work",
78
81
  itemRevision: relatedItem.revision
79
82
  } : {},
80
- event: "issueObserved",
83
+ event,
81
84
  issue
82
85
  };
83
86
  const rule = resolveFactoryLinearRule(this.options.rules, context.event);
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":["#ingestIssue"],"sources":["../../../src/integrations/linear/rules.ts"],"sourcesContent":["import { resolveFactoryLinearRule } from '../../rules/resolve.js';\nimport type { FactoryLinearRuleContext, FactoryRuleDecision, FactoryRules } from '../../rules/types.js';\nimport { validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport interface LinearIssueIngress {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n team: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface LinearRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n rules: FactoryRules;\n}\n\nexport interface LinearRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: LinearIssueIngress[];\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class LinearRules {\n constructor(private readonly options: LinearRulesOptions) {}\n\n async ingest(input: LinearRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n statuses.push(await this.#ingestIssue(input, issue, itemsBySourceKey.get(`linear:${issue.identifier}`)));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #ingestIssue(\n input: LinearRulesIngress,\n issue: LinearIssueIngress,\n relatedItem: WorkItemRow | undefined,\n ): Promise<IngressStatus> {\n const ingressId = `linear:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n const context: FactoryLinearRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'linear', id: ingressId },\n cause: 'linear.issueObserved',\n causalChain: [],\n ruleSetVersion: this.options.rules.version,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'linear-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n },\n board: 'work' as const,\n itemRevision: relatedItem.revision,\n }\n : {}),\n event: 'issueObserved',\n issue,\n };\n\n const rule = resolveFactoryLinearRule(this.options.rules, context.event);\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => ({ ...entry }));\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Linear rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'linear.issueObserved' },\n ruleSetVersion: this.options.rules.version,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n return committed.status;\n }\n}\n\nexport function attachLinearRules(\n context: IntegrationContext,\n): ((input: LinearRulesIngress) => Promise<unknown>) | undefined {\n if (!context.rules) return undefined;\n const rules = new LinearRules({\n projects: context.storage.projects,\n storage: context.rules.workItems,\n rules: context.rules.config,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","names":["#ingestIssue"],"sources":["../../../src/integrations/linear/rules.ts"],"sourcesContent":["import { resolveFactoryLinearRule } from '../../rules/resolve.js';\nimport type { FactoryLinearRuleContext, FactoryRuleDecision, FactoryRules } from '../../rules/types.js';\nimport { validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport interface LinearIssueIngress {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Linear user who created the issue, when available. */\n creator: string | null;\n team: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface LinearRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n rules: FactoryRules;\n}\n\nexport interface LinearRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: LinearIssueIngress[];\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class LinearRules {\n constructor(private readonly options: LinearRulesOptions) {}\n\n async ingest(input: LinearRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n statuses.push(await this.#ingestIssue(input, issue, itemsBySourceKey.get(`linear:${issue.identifier}`)));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #ingestIssue(\n input: LinearRulesIngress,\n issue: LinearIssueIngress,\n relatedItem: WorkItemRow | undefined,\n ): Promise<IngressStatus> {\n const ingressId = `linear:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const context: FactoryLinearRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'linear', id: ingressId },\n cause: `linear.${event}`,\n causalChain: [],\n ruleSetVersion: this.options.rules.version,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'linear-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n },\n board: 'work' as const,\n itemRevision: relatedItem.revision,\n }\n : {}),\n event,\n issue,\n };\n\n const rule = resolveFactoryLinearRule(this.options.rules, context.event);\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => ({ ...entry }));\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Linear rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'linear.issueObserved' },\n ruleSetVersion: this.options.rules.version,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n return committed.status;\n }\n}\n\nexport function attachLinearRules(\n context: IntegrationContext,\n): ((input: LinearRulesIngress) => Promise<unknown>) | undefined {\n if (!context.rules) return undefined;\n const rules = new LinearRules({\n projects: context.storage.projects,\n storage: context.rules.workItems,\n rules: context.rules.config,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ import type { MountedMastraCode } from '@mastra/code-sdk';
2
2
  import { MastraWorker } from '@mastra/core/worker';
3
3
  import type { WorkerDeps } from '@mastra/core/worker';
4
4
  import type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';
5
+ import type { GithubIssueReconciler } from '../../github/issue-reconciler.js';
5
6
  import type { GithubPullRequestReconciler } from '../../github/rules.js';
6
7
  import { dispatchGithubWebhook } from '../../github/webhook.js';
7
8
  import type { GithubWebhookDispatchIntegration, ParsedGithubWebhook } from '../../github/webhook.js';
@@ -24,6 +25,12 @@ export interface PlatformGithubEventWorkerConfig {
24
25
  storage: PlatformGithubEventStorage;
25
26
  ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;
26
27
  reconcileFactoryState?: GithubPullRequestReconciler;
28
+ /**
29
+ * Optional issue-metadata reconciler. When set, folded into the same
30
+ * reconcile tick as `reconcileFactoryState` so a single lease covers both
31
+ * writers of card state for the currently discovered repositories.
32
+ */
33
+ reconcileIssuesFactoryState?: GithubIssueReconciler;
27
34
  /** When false the worker skips event tailing and only runs the reconcile sweep. */
28
35
  pollEventsEnabled?: boolean;
29
36
  intervalMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"event-worker.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/event-worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+CAA+C,CAAC;AAE9F,OAAO,KAAK,EAAE,2BAA2B,EAAuB,MAAM,uBAAuB,CAAC;AAG9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EACV,gCAAgC,EAEhC,mBAAmB,EACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA2B1D,KAAK,WAAW,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC;AACzE,KAAK,iCAAiC,GAAG;IACvC,OAAO,EAAE,CAAC,CAAC;IACX,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,wBAAwB,CAC/D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvB,iCAAiC,EACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACxB,CAAC;AAWF,MAAM,MAAM,sCAAsC,GAAG,gCAAgC,CAAC;AAEtF,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC5C,MAAM,EAAE,sCAAsC,CAAC;IAC/C,OAAO,EAAE,0BAA0B,CAAC;IACpC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACtE,qBAAqB,CAAC,EAAE,2BAA2B,CAAC;IACpD,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,qBAAqB,CAAC;CACzC;AAED,qBAAa,yBAA0B,SAAQ,YAAY;;IACzD,QAAQ,CAAC,IAAI,4BAA4B;gBA0B7B,MAAM,EAAE,+BAA+B;IAmB7C,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,IAAI,SAAS,IAAI,OAAO,CAEvB;CA+QF"}
1
+ {"version":3,"file":"event-worker.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/event-worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+CAA+C,CAAC;AAE9F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAC9E,OAAO,KAAK,EAAE,2BAA2B,EAAuB,MAAM,uBAAuB,CAAC;AAG9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EACV,gCAAgC,EAEhC,mBAAmB,EACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA2B1D,KAAK,WAAW,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC;AACzE,KAAK,iCAAiC,GAAG;IACvC,OAAO,EAAE,CAAC,CAAC;IACX,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,wBAAwB,CAC/D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvB,iCAAiC,EACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACxB,CAAC;AAWF,MAAM,MAAM,sCAAsC,GAAG,gCAAgC,CAAC;AAEtF,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC5C,MAAM,EAAE,sCAAsC,CAAC;IAC/C,OAAO,EAAE,0BAA0B,CAAC;IACpC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACtE,qBAAqB,CAAC,EAAE,2BAA2B,CAAC;IACpD;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,qBAAqB,CAAC;IACpD,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,qBAAqB,CAAC;CACzC;AAED,qBAAa,yBAA0B,SAAQ,YAAY;;IACzD,QAAQ,CAAC,IAAI,4BAA4B;gBA2B7B,MAAM,EAAE,+BAA+B;IAoB7C,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,IAAI,SAAS,IAAI,OAAO,CAEvB;CAySF"}
@@ -40,6 +40,7 @@ var PlatformGithubEventWorker = class extends MastraWorker {
40
40
  #storage;
41
41
  #ingestFactoryEvent;
42
42
  #reconcileFactoryState;
43
+ #reconcileIssuesFactoryState;
43
44
  #pollEventsEnabled;
44
45
  #reconcileIntervalMs;
45
46
  #intervalMs;
@@ -67,6 +68,7 @@ var PlatformGithubEventWorker = class extends MastraWorker {
67
68
  this.#storage = config.storage;
68
69
  this.#ingestFactoryEvent = config.ingestFactoryEvent;
69
70
  this.#reconcileFactoryState = config.reconcileFactoryState;
71
+ this.#reconcileIssuesFactoryState = config.reconcileIssuesFactoryState;
70
72
  this.#pollEventsEnabled = config.pollEventsEnabled ?? true;
71
73
  this.#reconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;
72
74
  this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
@@ -223,6 +225,28 @@ var PlatformGithubEventWorker = class extends MastraWorker {
223
225
  error: error instanceof Error ? error.message : String(error)
224
226
  });
225
227
  }
228
+ if (!this.#reconcileIssuesFactoryState) return;
229
+ const issueStartedAt = Date.now();
230
+ try {
231
+ const { errors, ...counts } = await this.#reconcileIssuesFactoryState(targets);
232
+ const context = {
233
+ ...counts,
234
+ candidateRepositories: targets.length,
235
+ durationMs: Date.now() - issueStartedAt
236
+ };
237
+ if (counts.failed > 0) this.deps?.logger.warn("Platform GitHub issue reconcile sweep completed with failures", {
238
+ ...context,
239
+ errors
240
+ });
241
+ else if (counts.updated > 0) this.deps?.logger.info("Platform GitHub issue reconcile patched stale metadata", context);
242
+ else this.deps?.logger.debug("Platform GitHub issue reconcile sweep completed", context);
243
+ } catch (error) {
244
+ this.deps?.logger.error("Platform GitHub issue reconcile failed", {
245
+ repositories: targets.length,
246
+ durationMs: Date.now() - issueStartedAt,
247
+ error: error instanceof Error ? error.message : String(error)
248
+ });
249
+ }
226
250
  }
227
251
  async #discoverRepositories() {
228
252
  const result = await this.#client.request("GET", `${API_PREFIX}/installations`);
@@ -1 +1 @@
1
- {"version":3,"file":"event-worker.js","names":["#client","#controller","#github","#storage","#ingestFactoryEvent","#reconcileFactoryState","#pollEventsEnabled","#reconcileIntervalMs","#intervalMs","#now","#dispatch","#leaseOwner","#leaseTtlMs","#leaseProvider","#running","#startedAt","#settings","#schedule","#timer","#stopLeaseRenewal","#inFlight","#hasLease","#leaseKey","#tick","#ensureLease","#poll","#startLeaseRenewal","#leaseRenewalTimer","#discoverRepositories","#pollRepository","#maybeReconcile","#lastReconcileAt","#saveSettings","#isAuthorizedSender"],"sources":["../../../../src/integrations/platform/github/event-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';\nimport type { GithubRepositoryPermission } from '../../github/integration.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from '../../github/rules.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport type { GithubSubscriptionStorage } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook } from '../../github/webhook.js';\nimport type {\n GithubWebhookDispatchIntegration,\n GithubWebhookNotification,\n ParsedGithubWebhook,\n} from '../../github/webhook.js';\nimport type { PlatformApiClient } from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/github-app';\nconst DEFAULT_POLL_INTERVAL_MS = 20_000;\nconst DEFAULT_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_github_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst SUPPORTED_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment',\n 'pull-request-comment',\n 'pull-request-review',\n 'pull-request-review-comment',\n]);\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set<GithubRepositoryPermission>(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformGithubEventWorkerSettings = {\n version: 1;\n repositories: Record<string, EventCursor>;\n};\n\nexport type PlatformGithubEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformGithubEventWorkerSettings,\n Record<string, unknown>\n>;\n\ntype EventLogEntry = {\n id: string;\n deliveryId: string;\n event: string;\n payload: unknown;\n};\n\ntype Repository = { id: number; fullName?: string; installationId: number };\n\nexport type PlatformGithubEventDispatchIntegration = GithubWebhookDispatchIntegration;\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n reconcileFactoryState?: GithubPullRequestReconciler;\n /** When false the worker skips event tailing and only runs the reconcile sweep. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n reconcileIntervalMs?: number;\n now?: () => number;\n dispatch?: typeof dispatchGithubWebhook;\n}\n\nexport class PlatformGithubEventWorker extends MastraWorker {\n readonly name = 'platform-github-events';\n\n readonly #client: PlatformApiClient;\n readonly #controller: MountedMastraCode['controller'];\n readonly #github: PlatformGithubEventDispatchIntegration;\n readonly #storage: PlatformGithubEventStorage;\n readonly #ingestFactoryEvent: ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined;\n readonly #reconcileFactoryState: GithubPullRequestReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #reconcileIntervalMs: number;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #leaseRenewalTimer: ReturnType<typeof setInterval> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n #leaseTtlMs: number;\n #hasLease = false;\n #startedAt = 0;\n #lastReconcileAt = 0;\n #settings: PlatformGithubEventWorkerSettings = { version: 1, repositories: {} };\n\n constructor(config: PlatformGithubEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#controller = config.controller;\n this.#github = config.github;\n this.#storage = config.storage;\n this.#ingestFactoryEvent = config.ingestFactoryEvent;\n this.#reconcileFactoryState = config.reconcileFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n this.#reconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform GitHub event polling interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('PlatformGithubEventWorker: call init() before start()');\n\n this.#startedAt = this.#now() - 1;\n this.#settings = normalizeSettings(await this.#storage.settings.get(CURSOR_ORG_ID, CURSOR_USER_ID));\n this.#running = true;\n this.deps.logger.info('Platform GitHub event polling started', {\n intervalMs: this.#intervalMs,\n leaseTtlMs: this.#leaseTtlMs,\n });\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#stopLeaseRenewal();\n await this.#inFlight;\n if (this.#hasLease) {\n await this.#leaseProvider.releaseLease(this.#leaseKey(), this.#leaseOwner).catch(() => undefined);\n this.#hasLease = false;\n }\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick();\n this.#inFlight = run;\n void run.finally(() => {\n if (this.#inFlight === run) this.#inFlight = undefined;\n });\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n let nextDelay = this.#intervalMs;\n try {\n if (!(await this.#ensureLease())) return;\n nextDelay = await this.#poll();\n } catch (error) {\n nextDelay = retryDelay(error, this.#intervalMs);\n this.deps?.logger.error('Platform GitHub event polling cycle failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: nextDelay,\n });\n } finally {\n this.#schedule(nextDelay);\n }\n }\n\n async #ensureLease(): Promise<boolean> {\n if (this.#hasLease) return true;\n const result = await this.#leaseProvider.acquireLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs);\n this.#hasLease = result.acquired;\n if (this.#hasLease) this.#startLeaseRenewal();\n return this.#hasLease;\n }\n\n #startLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) return;\n this.#leaseRenewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n }\n })\n .catch(error => {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n this.deps?.logger.warn('Platform GitHub event polling lease renewal failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.floor(this.#leaseTtlMs / 3),\n );\n this.#leaseRenewalTimer.unref?.();\n }\n\n #stopLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) clearInterval(this.#leaseRenewalTimer);\n this.#leaseRenewalTimer = undefined;\n }\n\n async #poll(): Promise<number> {\n const repositories = await this.#discoverRepositories();\n // Reconcile-only mode has no event tail to keep fresh, so tick on the\n // slower reconcile cadence instead of the polling interval.\n let retryInMs = this.#pollEventsEnabled ? this.#intervalMs : this.#reconcileIntervalMs;\n\n if (this.#pollEventsEnabled) {\n for (const repository of repositories) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollRepository(repository.id);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform GitHub repository event polling failed', {\n repositoryId: repository.id,\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n if (error instanceof PlatformApiError && error.status === 429) break;\n }\n }\n }\n\n await this.#maybeReconcile(repositories);\n\n return retryInMs;\n }\n\n /**\n * State-based safety net: event tailing can miss merges (cursor gaps,\n * downtime, terminally failed decisions), so on a slower cadence we compare\n * still-open PR cards against actual GitHub state and replay missed merges\n * through the normal ingress, which dedupes against the event path.\n */\n async #maybeReconcile(repositories: Repository[]): Promise<void> {\n if (!this.#reconcileFactoryState || !this.#running || !this.#hasLease) return;\n const now = this.#now();\n if (now - this.#lastReconcileAt < this.#reconcileIntervalMs) return;\n // Advance the clock before sweeping so a persistently failing sweep stays\n // on cadence instead of retrying every poll tick.\n this.#lastReconcileAt = now;\n const targets: ReconcileRepository[] = repositories.flatMap(repository =>\n repository.fullName\n ? [{ id: repository.id, fullName: repository.fullName, installationId: repository.installationId }]\n : [],\n );\n if (targets.length === 0) {\n this.deps?.logger.debug('Platform GitHub pull request reconcile skipped: no named repositories');\n return;\n }\n this.deps?.logger.debug('Platform GitHub pull request reconcile sweep starting', {\n candidateRepositories: targets.length,\n });\n const startedAt = Date.now();\n try {\n // `counts.repositories` is the factory-configured subset actually swept;\n // `candidateRepositories` is everything the installations exposed.\n const { errors, ...counts } = await this.#reconcileFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub pull request reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.info('Platform GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub pull request reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n async #discoverRepositories(): Promise<Repository[]> {\n const result = await this.#client.request<{\n installations: Array<{\n installationId: number;\n usable: boolean;\n suspendedAt: string | null;\n }>;\n }>('GET', `${API_PREFIX}/installations`);\n const repositories = new Map<number, Repository>();\n\n for (const installation of result.installations) {\n if (!installation.usable || installation.suspendedAt) continue;\n const page = await this.#client.request<{ repositories: Array<{ id: number; fullName?: string }> }>(\n 'GET',\n `${API_PREFIX}/installations/${installation.installationId}/repositories`,\n );\n for (const repository of page.repositories) {\n repositories.set(repository.id, { ...repository, installationId: installation.installationId });\n }\n }\n\n return [...repositories.values()];\n }\n\n async #pollRepository(repositoryId: number): Promise<void> {\n const key = String(repositoryId);\n if (!this.#settings.repositories[key]) {\n this.#settings.repositories[key] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.repositories[key]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('afterTimestamp', String(cursor.afterTimestamp));\n\n const pollStartedAt = performance.now();\n const page = await this.#client.request<{ events: EventLogEntry[]; nextCursor: string | null }>(\n 'GET',\n `${API_PREFIX}/repositories/${repositoryId}/events?${query}`,\n );\n this.deps?.logger.debug('Platform GitHub repository event poll completed', {\n repositoryId,\n eventCount: page.events.length,\n latencyMs: Math.round(performance.now() - pollStartedAt),\n });\n if (page.events.length === 0 || !page.nextCursor) return;\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n const parsed = parseEvent(event);\n if (!parsed) {\n this.deps?.logger.warn('Platform GitHub event log returned a malformed event', {\n repositoryId,\n eventId: event.id,\n });\n continue;\n }\n if (isFactoryClosureEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n }\n const result = await this.#dispatch(parsed, {\n controller: this.#controller,\n listSubscriptions: (target, options) =>\n listPullRequestSubscriptionsForWebhook(target, options, this.#github.integrationStorage),\n retireSubscription: (id, status) =>\n retirePullRequestSubscription(id, status, this.#github.integrationStorage),\n github: this.#github,\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n subscriptionId: subscription.id,\n resourceId: subscription.resourceId,\n threadId: subscription.threadId,\n error: error instanceof Error ? error.message : String(error),\n });\n },\n });\n if (result.failed > 0) {\n throw new Error(\n `Platform GitHub event ${event.deliveryId} failed for ${result.failed} subscribed target(s).`,\n );\n }\n }\n\n if (page.nextCursor === ('afterEventId' in cursor ? cursor.afterEventId : undefined)) return;\n this.#settings.repositories[key] = { afterEventId: page.nextCursor };\n await this.#saveSettings();\n }\n }\n\n async #isAuthorizedSender(notification: GithubWebhookNotification): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n if (AUTHORIZED_BOTS.has(sender)) return true;\n\n const abortController = new AbortController();\n const timeout = setTimeout(() => abortController.abort(), PERMISSION_CHECK_TIMEOUT_MS);\n try {\n const permission = await this.#github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n );\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n async #saveSettings(): Promise<void> {\n await this.#storage.settings.save(CURSOR_ORG_ID, CURSOR_USER_ID, this.#settings);\n }\n\n #leaseKey(): string {\n return `${this.name}:${this.#storage.integrationId}`;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n\nfunction normalizeSettings(value: PlatformGithubEventWorkerSettings | null): PlatformGithubEventWorkerSettings {\n if (!value || value.version !== 1 || !value.repositories || typeof value.repositories !== 'object') {\n return { version: 1, repositories: {} };\n }\n return { version: 1, repositories: { ...value.repositories } };\n}\n\nfunction isFactoryClosureEvent(event: ParsedGithubWebhook): boolean {\n return (event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed';\n}\n\nfunction parseEvent(event: EventLogEntry): ParsedGithubWebhook | null {\n if (\n !event.id ||\n !event.deliveryId ||\n !SUPPORTED_EVENTS.has(event.event) ||\n !event.payload ||\n typeof event.payload !== 'object' ||\n Array.isArray(event.payload)\n ) {\n return null;\n }\n return {\n event: event.event,\n deliveryId: event.deliveryId,\n payload: event.payload as Record<string, unknown>,\n };\n}\n\nfunction retryDelay(error: unknown, fallbackMs: number): number {\n if (error instanceof PlatformApiError && error.status === 429 && error.retryAfterSeconds !== null) {\n return Math.max(fallbackMs, error.retryAfterSeconds * 1_000);\n }\n return fallbackMs;\n}\n"],"mappings":";;;;;;;AAsBA,MAAM,aAAa;AACnB,MAAM,2BAA2B;AACjC,MAAM,gCAAgC,IAAI;AAC1C,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AACzB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,mCAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,qCAAqB,IAAI,IAAI;CACjC;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,kCAAkB,IAAI,IAAI,CAAC,qBAAqB,2BAA2B,CAAC;AAClF,MAAM,yCAAyB,IAAI,IAAgC;CAAC;CAAS;CAAY;AAAO,CAAC;AACjG,MAAM,8BAA8B;AAwCpC,IAAa,4BAAb,cAA+C,aAAa;CAC1D,OAAgB;CAEhB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,cAAuB,WAAW;CAElC,WAAW;CACX;CACA;CACA;CACA,iBAAgC;CAChC;CACA,YAAY;CACZ,aAAa;CACb,mBAAmB;CACnB,YAA+C;EAAE,SAAS;EAAG,cAAc,CAAC;CAAE;CAE9E,YAAY,QAAyC;EACnD,MAAM;EACN,KAAKA,UAAU,OAAO;EACtB,KAAKC,cAAc,OAAO;EAC1B,KAAKC,UAAU,OAAO;EACtB,KAAKC,WAAW,OAAO;EACvB,KAAKC,sBAAsB,OAAO;EAClC,KAAKC,yBAAyB,OAAO;EACrC,KAAKC,qBAAqB,OAAO,qBAAqB;EACtD,KAAKC,uBAAuB,OAAO,uBAAuB;EAC1D,KAAKC,cAAc,OAAO,cAAc;EACxC,IAAI,CAAC,OAAO,SAAS,KAAKA,WAAW,KAAK,KAAKA,eAAe,GAC5D,MAAM,IAAI,MAAM,mEAAmE;EAErF,KAAKI,cAAc,KAAK,IAAI,kBAAkB,KAAKJ,cAAc,CAAC;EAClE,KAAKC,OAAO,OAAO,OAAO,KAAK;EAC/B,KAAKC,YAAY,OAAO,YAAY;CACtC;CAEA,MAAM,KAAK,MAAiC;EAC1C,MAAM,MAAM,KAAK,IAAI;EACrB,KAAKG,iBAAiB,iBAAiB,KAAK,MAAM;CACpD;CAEA,MAAM,QAAuB;EAC3B,IAAI,KAAKC,UAAU;EACnB,IAAI,CAAC,KAAK,MAAM,MAAM,IAAI,MAAM,uDAAuD;EAEvF,KAAKC,aAAa,KAAKN,KAAK,IAAI;EAChC,KAAKO,YAAY,kBAAkB,MAAM,KAAKb,SAAS,SAAS,IAAI,eAAe,cAAc,CAAC;EAClG,KAAKW,WAAW;EAChB,KAAK,KAAK,OAAO,KAAK,yCAAyC;GAC7D,YAAY,KAAKN;GACjB,YAAY,KAAKI;EACnB,CAAC;EACD,KAAKK,UAAU,CAAC;CAClB;CAEA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAKH,UAAU;EACpB,KAAKA,WAAW;EAChB,IAAI,KAAKI,QAAQ,aAAa,KAAKA,MAAM;EACzC,KAAKA,SAAS,KAAA;EACd,KAAKC,kBAAkB;EACvB,MAAM,KAAKC;EACX,IAAI,KAAKC,WAAW;GAClB,MAAM,KAAKR,eAAe,aAAa,KAAKS,UAAU,GAAG,KAAKX,WAAW,CAAC,CAAC,YAAY,KAAA,CAAS;GAChG,KAAKU,YAAY;EACnB;CACF;CAEA,IAAI,YAAqB;EACvB,OAAO,KAAKP;CACd;CAEA,UAAU,SAAuB;EAC/B,IAAI,CAAC,KAAKA,UAAU;EACpB,KAAKI,SAAS,iBAAiB;GAC7B,KAAKA,SAAS,KAAA;GACd,MAAM,MAAM,KAAKK,MAAM;GACvB,KAAKH,YAAY;GACjB,IAAS,cAAc;IACrB,IAAI,KAAKA,cAAc,KAAK,KAAKA,YAAY,KAAA;GAC/C,CAAC;EACH,GAAG,OAAO;EACV,KAAKF,OAAO,QAAQ;CACtB;CAEA,MAAMK,QAAuB;EAC3B,IAAI,YAAY,KAAKf;EACrB,IAAI;GACF,IAAI,CAAE,MAAM,KAAKgB,aAAa,GAAI;GAClC,YAAY,MAAM,KAAKC,MAAM;EAC/B,SAAS,OAAO;GACd,YAAY,WAAW,OAAO,KAAKjB,WAAW;GAC9C,KAAK,MAAM,OAAO,MAAM,8CAA8C;IACpE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC5D,WAAW;GACb,CAAC;EACH,UAAU;GACR,KAAKS,UAAU,SAAS;EAC1B;CACF;CAEA,MAAMO,eAAiC;EACrC,IAAI,KAAKH,WAAW,OAAO;EAC3B,MAAM,SAAS,MAAM,KAAKR,eAAe,aAAa,KAAKS,UAAU,GAAG,KAAKX,aAAa,KAAKC,WAAW;EAC1G,KAAKS,YAAY,OAAO;EACxB,IAAI,KAAKA,WAAW,KAAKK,mBAAmB;EAC5C,OAAO,KAAKL;CACd;CAEA,qBAA2B;EACzB,IAAI,KAAKM,oBAAoB;EAC7B,KAAKA,qBAAqB,kBAClB;GACJ,KAAUd,eACP,WAAW,KAAKS,UAAU,GAAG,KAAKX,aAAa,KAAKC,WAAW,CAAC,CAChE,MAAK,YAAW;IACf,IAAI,CAAC,SAAS;KACZ,KAAKS,YAAY;KACjB,KAAKF,kBAAkB;IACzB;GACF,CAAC,CAAC,CACD,OAAM,UAAS;IACd,KAAKE,YAAY;IACjB,KAAKF,kBAAkB;IACvB,KAAK,MAAM,OAAO,KAAK,sDAAsD,EAC3E,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;GACH,CAAC;EACL,GACA,KAAK,MAAM,KAAKP,cAAc,CAAC,CACjC;EACA,KAAKe,mBAAmB,QAAQ;CAClC;CAEA,oBAA0B;EACxB,IAAI,KAAKA,oBAAoB,cAAc,KAAKA,kBAAkB;EAClE,KAAKA,qBAAqB,KAAA;CAC5B;CAEA,MAAMF,QAAyB;EAC7B,MAAM,eAAe,MAAM,KAAKG,sBAAsB;EAGtD,IAAI,YAAY,KAAKtB,qBAAqB,KAAKE,cAAc,KAAKD;EAElE,IAAI,KAAKD,oBACP,KAAK,MAAM,cAAc,cAAc;GACrC,IAAI,CAAC,KAAKQ,YAAY,CAAC,KAAKO,WAAW;GACvC,IAAI;IACF,MAAM,KAAKQ,gBAAgB,WAAW,EAAE;GAC1C,SAAS,OAAO;IACd,MAAM,QAAQ,WAAW,OAAO,KAAKrB,WAAW;IAChD,YAAY,KAAK,IAAI,WAAW,KAAK;IACrC,KAAK,MAAM,OAAO,MAAM,mDAAmD;KACzE,cAAc,WAAW;KACzB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KAC5D,WAAW;IACb,CAAC;IACD,IAAI,iBAAiB,oBAAoB,MAAM,WAAW,KAAK;GACjE;EACF;EAGF,MAAM,KAAKsB,gBAAgB,YAAY;EAEvC,OAAO;CACT;;;;;;;CAQA,MAAMA,gBAAgB,cAA2C;EAC/D,IAAI,CAAC,KAAKzB,0BAA0B,CAAC,KAAKS,YAAY,CAAC,KAAKO,WAAW;EACvE,MAAM,MAAM,KAAKZ,KAAK;EACtB,IAAI,MAAM,KAAKsB,mBAAmB,KAAKxB,sBAAsB;EAG7D,KAAKwB,mBAAmB;EACxB,MAAM,UAAiC,aAAa,SAAQ,eAC1D,WAAW,WACP,CAAC;GAAE,IAAI,WAAW;GAAI,UAAU,WAAW;GAAU,gBAAgB,WAAW;EAAe,CAAC,IAChG,CAAC,CACP;EACA,IAAI,QAAQ,WAAW,GAAG;GACxB,KAAK,MAAM,OAAO,MAAM,uEAAuE;GAC/F;EACF;EACA,KAAK,MAAM,OAAO,MAAM,yDAAyD,EAC/E,uBAAuB,QAAQ,OACjC,CAAC;EACD,MAAM,YAAY,KAAK,IAAI;EAC3B,IAAI;GAGF,MAAM,EAAE,QAAQ,GAAG,WAAW,MAAM,KAAK1B,uBAAuB,OAAO;GACvE,MAAM,UAAU;IAAE,GAAG;IAAQ,uBAAuB,QAAQ;IAAQ,YAAY,KAAK,IAAI,IAAI;GAAU;GACvG,IAAI,OAAO,SAAS,GAClB,KAAK,MAAM,OAAO,KAAK,wEAAwE;IAC7F,GAAG;IACH;GACF,CAAC;QACI,IAAI,OAAO,SAAS,KAAK,OAAO,SAAS,GAC9C,KAAK,MAAM,OAAO,KAAK,wEAAwE,OAAO;QAEtG,KAAK,MAAM,OAAO,KAAK,0DAA0D,OAAO;EAE5F,SAAS,OAAO;GACd,KAAK,MAAM,OAAO,MAAM,iDAAiD;IACvE,cAAc,QAAQ;IACtB,YAAY,KAAK,IAAI,IAAI;IACzB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAAC;EACH;CACF;CAEA,MAAMuB,wBAA+C;EACnD,MAAM,SAAS,MAAM,KAAK5B,QAAQ,QAM/B,OAAO,GAAG,WAAW,eAAe;EACvC,MAAM,+BAAe,IAAI,IAAwB;EAEjD,KAAK,MAAM,gBAAgB,OAAO,eAAe;GAC/C,IAAI,CAAC,aAAa,UAAU,aAAa,aAAa;GACtD,MAAM,OAAO,MAAM,KAAKA,QAAQ,QAC9B,OACA,GAAG,WAAW,iBAAiB,aAAa,eAAe,cAC7D;GACA,KAAK,MAAM,cAAc,KAAK,cAC5B,aAAa,IAAI,WAAW,IAAI;IAAE,GAAG;IAAY,gBAAgB,aAAa;GAAe,CAAC;EAElG;EAEA,OAAO,CAAC,GAAG,aAAa,OAAO,CAAC;CAClC;CAEA,MAAM6B,gBAAgB,cAAqC;EACzD,MAAM,MAAM,OAAO,YAAY;EAC/B,IAAI,CAAC,KAAKb,UAAU,aAAa,MAAM;GACrC,KAAKA,UAAU,aAAa,OAAO,EAAE,gBAAgB,KAAKD,WAAW;GACrE,MAAM,KAAKiB,cAAc;EAC3B;EAEA,OAAO,KAAKlB,YAAY,KAAKO,WAAW;GACtC,MAAM,SAAsB,KAAKL,UAAU,aAAa;GACxD,MAAM,QAAQ,IAAI,gBAAgB,EAAE,OAAO,OAAO,eAAe,EAAE,CAAC;GACpE,IAAI,kBAAkB,QAAQ,MAAM,IAAI,gBAAgB,OAAO,YAAY;QACtE,MAAM,IAAI,kBAAkB,OAAO,OAAO,cAAc,CAAC;GAE9D,MAAM,gBAAgB,YAAY,IAAI;GACtC,MAAM,OAAO,MAAM,KAAKhB,QAAQ,QAC9B,OACA,GAAG,WAAW,gBAAgB,aAAa,UAAU,OACvD;GACA,KAAK,MAAM,OAAO,MAAM,mDAAmD;IACzE;IACA,YAAY,KAAK,OAAO;IACxB,WAAW,KAAK,MAAM,YAAY,IAAI,IAAI,aAAa;GACzD,CAAC;GACD,IAAI,KAAK,OAAO,WAAW,KAAK,CAAC,KAAK,YAAY;GAElD,KAAK,MAAM,SAAS,KAAK,QAAQ;IAC/B,IAAI,CAAC,KAAKc,YAAY,CAAC,KAAKO,WAAW;IACvC,MAAM,SAAS,WAAW,KAAK;IAC/B,IAAI,CAAC,QAAQ;KACX,KAAK,MAAM,OAAO,KAAK,wDAAwD;MAC7E;MACA,SAAS,MAAM;KACjB,CAAC;KACD;IACF;IACA,IAAI,sBAAsB,MAAM,GAC9B,MAAM,KAAKjB,sBAAsB,MAAM;IAEzC,MAAM,SAAS,MAAM,KAAKM,UAAU,QAAQ;KAC1C,YAAY,KAAKT;KACjB,oBAAoB,QAAQ,YAC1B,uCAAuC,QAAQ,SAAS,KAAKC,QAAQ,kBAAkB;KACzF,qBAAqB,IAAI,WACvB,8BAA8B,IAAI,QAAQ,KAAKA,QAAQ,kBAAkB;KAC3E,QAAQ,KAAKA;KACb,qBAAoB,iBAAgB,KAAK+B,oBAAoB,YAAY;KACzE,gBAAgB,cAAc,UAAU;MACtC,KAAK,MAAM,OAAO,MAAM,4DAA4D;OAClF,gBAAgB,aAAa;OAC7B,YAAY,aAAa;OACzB,UAAU,aAAa;OACvB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;MAC9D,CAAC;KACH;IACF,CAAC;IACD,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,MACR,yBAAyB,MAAM,WAAW,cAAc,OAAO,OAAO,uBACxE;GAEJ;GAEA,IAAI,KAAK,gBAAgB,kBAAkB,SAAS,OAAO,eAAe,KAAA,IAAY;GACtF,KAAKjB,UAAU,aAAa,OAAO,EAAE,cAAc,KAAK,WAAW;GACnE,MAAM,KAAKgB,cAAc;EAC3B;CACF;CAEA,MAAMC,oBAAoB,cAA2D;EACnF,IAAI,CAAC,mBAAmB,IAAI,aAAa,IAAI,GAAG,OAAO;EACvD,MAAM,SAAS,aAAa,SAAS;EACrC,MAAM,aAAa,aAAa,SAAS;EACzC,IAAI,CAAC,UAAU,CAAC,YAAY,OAAO;EACnC,IAAI,gBAAgB,IAAI,MAAM,GAAG,OAAO;EAExC,MAAM,kBAAkB,IAAI,gBAAgB;EAC5C,MAAM,UAAU,iBAAiB,gBAAgB,MAAM,GAAG,2BAA2B;EACrF,IAAI;GACF,MAAM,aAAa,MAAM,KAAK/B,QAAQ,oCACpC,aAAa,SAAS,gBACtB,YACA,QACA,gBAAgB,MAClB;GACA,OAAO,eAAe,KAAA,KAAa,uBAAuB,IAAI,UAAU;EAC1E,QAAQ;GACN,OAAO;EACT,UAAU;GACR,aAAa,OAAO;EACtB;CACF;CAEA,MAAM8B,gBAA+B;EACnC,MAAM,KAAK7B,SAAS,SAAS,KAAK,eAAe,gBAAgB,KAAKa,SAAS;CACjF;CAEA,YAAoB;EAClB,OAAO,GAAG,KAAK,KAAK,GAAG,KAAKb,SAAS;CACvC;AACF;AAEA,SAAS,iBAAiB,QAA+B;CACvD,MAAM,cAAe,OAA2E;CAChG,IAAI,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,MAAM,KAAK;CAC1E,OAAO,gBAAgB,MAAM,IAAI,SAAS;AAC5C;AAEA,SAAS,kBAAkB,OAAoF;CAC7G,IAAI,CAAC,SAAS,MAAM,YAAY,KAAK,CAAC,MAAM,gBAAgB,OAAO,MAAM,iBAAiB,UACxF,OAAO;EAAE,SAAS;EAAG,cAAc,CAAC;CAAE;CAExC,OAAO;EAAE,SAAS;EAAG,cAAc,EAAE,GAAG,MAAM,aAAa;CAAE;AAC/D;AAEA,SAAS,sBAAsB,OAAqC;CAClE,QAAQ,MAAM,UAAU,YAAY,MAAM,UAAU,mBAAmB,MAAM,QAAQ,WAAW;AAClG;AAEA,SAAS,WAAW,OAAkD;CACpE,IACE,CAAC,MAAM,MACP,CAAC,MAAM,cACP,CAAC,iBAAiB,IAAI,MAAM,KAAK,KACjC,CAAC,MAAM,WACP,OAAO,MAAM,YAAY,YACzB,MAAM,QAAQ,MAAM,OAAO,GAE3B,OAAO;CAET,OAAO;EACL,OAAO,MAAM;EACb,YAAY,MAAM;EAClB,SAAS,MAAM;CACjB;AACF;AAEA,SAAS,WAAW,OAAgB,YAA4B;CAC9D,IAAI,iBAAiB,oBAAoB,MAAM,WAAW,OAAO,MAAM,sBAAsB,MAC3F,OAAO,KAAK,IAAI,YAAY,MAAM,oBAAoB,GAAK;CAE7D,OAAO;AACT"}
1
+ {"version":3,"file":"event-worker.js","names":["#client","#controller","#github","#storage","#ingestFactoryEvent","#reconcileFactoryState","#reconcileIssuesFactoryState","#pollEventsEnabled","#reconcileIntervalMs","#intervalMs","#now","#dispatch","#leaseOwner","#leaseTtlMs","#leaseProvider","#running","#startedAt","#settings","#schedule","#timer","#stopLeaseRenewal","#inFlight","#hasLease","#leaseKey","#tick","#ensureLease","#poll","#startLeaseRenewal","#leaseRenewalTimer","#discoverRepositories","#pollRepository","#maybeReconcile","#lastReconcileAt","#saveSettings","#isAuthorizedSender"],"sources":["../../../../src/integrations/platform/github/event-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';\nimport type { GithubRepositoryPermission } from '../../github/integration.js';\nimport type { GithubIssueReconciler } from '../../github/issue-reconciler.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from '../../github/rules.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport type { GithubSubscriptionStorage } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook } from '../../github/webhook.js';\nimport type {\n GithubWebhookDispatchIntegration,\n GithubWebhookNotification,\n ParsedGithubWebhook,\n} from '../../github/webhook.js';\nimport type { PlatformApiClient } from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/github-app';\nconst DEFAULT_POLL_INTERVAL_MS = 20_000;\nconst DEFAULT_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_github_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst SUPPORTED_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment',\n 'pull-request-comment',\n 'pull-request-review',\n 'pull-request-review-comment',\n]);\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set<GithubRepositoryPermission>(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformGithubEventWorkerSettings = {\n version: 1;\n repositories: Record<string, EventCursor>;\n};\n\nexport type PlatformGithubEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformGithubEventWorkerSettings,\n Record<string, unknown>\n>;\n\ntype EventLogEntry = {\n id: string;\n deliveryId: string;\n event: string;\n payload: unknown;\n};\n\ntype Repository = { id: number; fullName?: string; installationId: number };\n\nexport type PlatformGithubEventDispatchIntegration = GithubWebhookDispatchIntegration;\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n reconcileFactoryState?: GithubPullRequestReconciler;\n /**\n * Optional issue-metadata reconciler. When set, folded into the same\n * reconcile tick as `reconcileFactoryState` so a single lease covers both\n * writers of card state for the currently discovered repositories.\n */\n reconcileIssuesFactoryState?: GithubIssueReconciler;\n /** When false the worker skips event tailing and only runs the reconcile sweep. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n reconcileIntervalMs?: number;\n now?: () => number;\n dispatch?: typeof dispatchGithubWebhook;\n}\n\nexport class PlatformGithubEventWorker extends MastraWorker {\n readonly name = 'platform-github-events';\n\n readonly #client: PlatformApiClient;\n readonly #controller: MountedMastraCode['controller'];\n readonly #github: PlatformGithubEventDispatchIntegration;\n readonly #storage: PlatformGithubEventStorage;\n readonly #ingestFactoryEvent: ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined;\n readonly #reconcileFactoryState: GithubPullRequestReconciler | undefined;\n readonly #reconcileIssuesFactoryState: GithubIssueReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #reconcileIntervalMs: number;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #leaseRenewalTimer: ReturnType<typeof setInterval> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n #leaseTtlMs: number;\n #hasLease = false;\n #startedAt = 0;\n #lastReconcileAt = 0;\n #settings: PlatformGithubEventWorkerSettings = { version: 1, repositories: {} };\n\n constructor(config: PlatformGithubEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#controller = config.controller;\n this.#github = config.github;\n this.#storage = config.storage;\n this.#ingestFactoryEvent = config.ingestFactoryEvent;\n this.#reconcileFactoryState = config.reconcileFactoryState;\n this.#reconcileIssuesFactoryState = config.reconcileIssuesFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n this.#reconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform GitHub event polling interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('PlatformGithubEventWorker: call init() before start()');\n\n this.#startedAt = this.#now() - 1;\n this.#settings = normalizeSettings(await this.#storage.settings.get(CURSOR_ORG_ID, CURSOR_USER_ID));\n this.#running = true;\n this.deps.logger.info('Platform GitHub event polling started', {\n intervalMs: this.#intervalMs,\n leaseTtlMs: this.#leaseTtlMs,\n });\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#stopLeaseRenewal();\n await this.#inFlight;\n if (this.#hasLease) {\n await this.#leaseProvider.releaseLease(this.#leaseKey(), this.#leaseOwner).catch(() => undefined);\n this.#hasLease = false;\n }\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick();\n this.#inFlight = run;\n void run.finally(() => {\n if (this.#inFlight === run) this.#inFlight = undefined;\n });\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n let nextDelay = this.#intervalMs;\n try {\n if (!(await this.#ensureLease())) return;\n nextDelay = await this.#poll();\n } catch (error) {\n nextDelay = retryDelay(error, this.#intervalMs);\n this.deps?.logger.error('Platform GitHub event polling cycle failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: nextDelay,\n });\n } finally {\n this.#schedule(nextDelay);\n }\n }\n\n async #ensureLease(): Promise<boolean> {\n if (this.#hasLease) return true;\n const result = await this.#leaseProvider.acquireLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs);\n this.#hasLease = result.acquired;\n if (this.#hasLease) this.#startLeaseRenewal();\n return this.#hasLease;\n }\n\n #startLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) return;\n this.#leaseRenewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n }\n })\n .catch(error => {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n this.deps?.logger.warn('Platform GitHub event polling lease renewal failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.floor(this.#leaseTtlMs / 3),\n );\n this.#leaseRenewalTimer.unref?.();\n }\n\n #stopLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) clearInterval(this.#leaseRenewalTimer);\n this.#leaseRenewalTimer = undefined;\n }\n\n async #poll(): Promise<number> {\n const repositories = await this.#discoverRepositories();\n // Reconcile-only mode has no event tail to keep fresh, so tick on the\n // slower reconcile cadence instead of the polling interval.\n let retryInMs = this.#pollEventsEnabled ? this.#intervalMs : this.#reconcileIntervalMs;\n\n if (this.#pollEventsEnabled) {\n for (const repository of repositories) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollRepository(repository.id);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform GitHub repository event polling failed', {\n repositoryId: repository.id,\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n if (error instanceof PlatformApiError && error.status === 429) break;\n }\n }\n }\n\n await this.#maybeReconcile(repositories);\n\n return retryInMs;\n }\n\n /**\n * State-based safety net: event tailing can miss merges (cursor gaps,\n * downtime, terminally failed decisions), so on a slower cadence we compare\n * still-open PR cards against actual GitHub state and replay missed merges\n * through the normal ingress, which dedupes against the event path.\n */\n async #maybeReconcile(repositories: Repository[]): Promise<void> {\n if (!this.#reconcileFactoryState || !this.#running || !this.#hasLease) return;\n const now = this.#now();\n if (now - this.#lastReconcileAt < this.#reconcileIntervalMs) return;\n // Advance the clock before sweeping so a persistently failing sweep stays\n // on cadence instead of retrying every poll tick.\n this.#lastReconcileAt = now;\n const targets: ReconcileRepository[] = repositories.flatMap(repository =>\n repository.fullName\n ? [{ id: repository.id, fullName: repository.fullName, installationId: repository.installationId }]\n : [],\n );\n if (targets.length === 0) {\n this.deps?.logger.debug('Platform GitHub pull request reconcile skipped: no named repositories');\n return;\n }\n this.deps?.logger.debug('Platform GitHub pull request reconcile sweep starting', {\n candidateRepositories: targets.length,\n });\n const startedAt = Date.now();\n try {\n // `counts.repositories` is the factory-configured subset actually swept;\n // `candidateRepositories` is everything the installations exposed.\n const { errors, ...counts } = await this.#reconcileFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub pull request reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.info('Platform GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub pull request reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n\n // Same tick as the PR reconciler: repository set already discovered, lease\n // already held, cadence already gated. Issues have no closed-webhook replay\n // so this only patches drifted metadata (state/author/assignees/labels).\n if (!this.#reconcileIssuesFactoryState) return;\n const issueStartedAt = Date.now();\n try {\n const { errors, ...counts } = await this.#reconcileIssuesFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - issueStartedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub issue reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.updated > 0) {\n this.deps?.logger.info('Platform GitHub issue reconcile patched stale metadata', context);\n } else {\n this.deps?.logger.debug('Platform GitHub issue reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub issue reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - issueStartedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n async #discoverRepositories(): Promise<Repository[]> {\n const result = await this.#client.request<{\n installations: Array<{\n installationId: number;\n usable: boolean;\n suspendedAt: string | null;\n }>;\n }>('GET', `${API_PREFIX}/installations`);\n const repositories = new Map<number, Repository>();\n\n for (const installation of result.installations) {\n if (!installation.usable || installation.suspendedAt) continue;\n const page = await this.#client.request<{ repositories: Array<{ id: number; fullName?: string }> }>(\n 'GET',\n `${API_PREFIX}/installations/${installation.installationId}/repositories`,\n );\n for (const repository of page.repositories) {\n repositories.set(repository.id, { ...repository, installationId: installation.installationId });\n }\n }\n\n return [...repositories.values()];\n }\n\n async #pollRepository(repositoryId: number): Promise<void> {\n const key = String(repositoryId);\n if (!this.#settings.repositories[key]) {\n this.#settings.repositories[key] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.repositories[key]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('afterTimestamp', String(cursor.afterTimestamp));\n\n const pollStartedAt = performance.now();\n const page = await this.#client.request<{ events: EventLogEntry[]; nextCursor: string | null }>(\n 'GET',\n `${API_PREFIX}/repositories/${repositoryId}/events?${query}`,\n );\n this.deps?.logger.debug('Platform GitHub repository event poll completed', {\n repositoryId,\n eventCount: page.events.length,\n latencyMs: Math.round(performance.now() - pollStartedAt),\n });\n if (page.events.length === 0 || !page.nextCursor) return;\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n const parsed = parseEvent(event);\n if (!parsed) {\n this.deps?.logger.warn('Platform GitHub event log returned a malformed event', {\n repositoryId,\n eventId: event.id,\n });\n continue;\n }\n if (isFactoryClosureEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n }\n const result = await this.#dispatch(parsed, {\n controller: this.#controller,\n listSubscriptions: (target, options) =>\n listPullRequestSubscriptionsForWebhook(target, options, this.#github.integrationStorage),\n retireSubscription: (id, status) =>\n retirePullRequestSubscription(id, status, this.#github.integrationStorage),\n github: this.#github,\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n subscriptionId: subscription.id,\n resourceId: subscription.resourceId,\n threadId: subscription.threadId,\n error: error instanceof Error ? error.message : String(error),\n });\n },\n });\n if (result.failed > 0) {\n throw new Error(\n `Platform GitHub event ${event.deliveryId} failed for ${result.failed} subscribed target(s).`,\n );\n }\n }\n\n if (page.nextCursor === ('afterEventId' in cursor ? cursor.afterEventId : undefined)) return;\n this.#settings.repositories[key] = { afterEventId: page.nextCursor };\n await this.#saveSettings();\n }\n }\n\n async #isAuthorizedSender(notification: GithubWebhookNotification): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n if (AUTHORIZED_BOTS.has(sender)) return true;\n\n const abortController = new AbortController();\n const timeout = setTimeout(() => abortController.abort(), PERMISSION_CHECK_TIMEOUT_MS);\n try {\n const permission = await this.#github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n );\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n async #saveSettings(): Promise<void> {\n await this.#storage.settings.save(CURSOR_ORG_ID, CURSOR_USER_ID, this.#settings);\n }\n\n #leaseKey(): string {\n return `${this.name}:${this.#storage.integrationId}`;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n\nfunction normalizeSettings(value: PlatformGithubEventWorkerSettings | null): PlatformGithubEventWorkerSettings {\n if (!value || value.version !== 1 || !value.repositories || typeof value.repositories !== 'object') {\n return { version: 1, repositories: {} };\n }\n return { version: 1, repositories: { ...value.repositories } };\n}\n\nfunction isFactoryClosureEvent(event: ParsedGithubWebhook): boolean {\n return (event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed';\n}\n\nfunction parseEvent(event: EventLogEntry): ParsedGithubWebhook | null {\n if (\n !event.id ||\n !event.deliveryId ||\n !SUPPORTED_EVENTS.has(event.event) ||\n !event.payload ||\n typeof event.payload !== 'object' ||\n Array.isArray(event.payload)\n ) {\n return null;\n }\n return {\n event: event.event,\n deliveryId: event.deliveryId,\n payload: event.payload as Record<string, unknown>,\n };\n}\n\nfunction retryDelay(error: unknown, fallbackMs: number): number {\n if (error instanceof PlatformApiError && error.status === 429 && error.retryAfterSeconds !== null) {\n return Math.max(fallbackMs, error.retryAfterSeconds * 1_000);\n }\n return fallbackMs;\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import type { RequestContext } from '@mastra/core/request-context';
2
2
  import type { ApiRoute } from '@mastra/core/server';
3
+ import type { MastraWorker } from '@mastra/core/worker';
3
4
  import type { Intake } from '../../../capabilities/intake.js';
4
5
  import type { VersionControl } from '../../../capabilities/version-control.js';
5
6
  import type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';
@@ -7,9 +8,8 @@ import type { SourceControlStorageHandle } from '../../../storage/domains/source
7
8
  import type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../../base.js';
8
9
  import type { GithubIntegration, GithubRepositoryPermission, RepoSummary } from '../../github/integration.js';
9
10
  import type { GithubIssueTriageInput, GithubIssueTriageResult } from '../../github/issue-triage.js';
10
- import type { ReconcilePullRequestState } from '../../github/rules.js';
11
+ import type { ReconcileIssueState, ReconcilePullRequestState } from '../../github/rules.js';
11
12
  import type { GithubSubscriptionStorage } from '../../github/subscriptions.js';
12
- import { PlatformGithubEventWorker } from './event-worker.js';
13
13
  export declare class PlatformGithubIntegration implements FactoryIntegration {
14
14
  #private;
15
15
  readonly id = "github";
@@ -29,7 +29,7 @@ export declare class PlatformGithubIntegration implements FactoryIntegration {
29
29
  storage: IntegrationStorageHandle;
30
30
  }): void;
31
31
  routes(ctx: IntegrationContext): ApiRoute[];
32
- workers(ctx: IntegrationContext): PlatformGithubEventWorker[];
32
+ workers(ctx: IntegrationContext): MastraWorker[];
33
33
  /**
34
34
  * Reads live PR state through the Platform GitHub proxy for the merge
35
35
  * reconciler. Returns undefined when the PR cannot be resolved (missing,
@@ -40,6 +40,16 @@ export declare class PlatformGithubIntegration implements FactoryIntegration {
40
40
  repository: string;
41
41
  number: number;
42
42
  }): Promise<ReconcilePullRequestState | undefined>;
43
+ /**
44
+ * Reads live issue state through the Platform GitHub proxy for the
45
+ * reconciler. Returns undefined when the issue cannot be resolved (missing,
46
+ * is actually a PR, proxy error) so a sweep never fabricates a close.
47
+ */
48
+ fetchIssueState(input: {
49
+ installationId: number;
50
+ repository: string;
51
+ number: number;
52
+ }): Promise<ReconcileIssueState | undefined>;
43
53
  sessionTools({ requestContext }: {
44
54
  requestContext: RequestContext;
45
55
  }): IntegrationTools;
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/integration.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAIxD,OAAO,KAAK,EAEV,MAAM,EAIP,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAwBV,cAAc,EACf,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+CAA+C,CAAC;AAC9F,OAAO,KAAK,EAEV,0BAA0B,EAC3B,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAIpG,OAAO,KAAK,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAM5F,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAoI/E,qBAAa,yBAA0B,YAAW,kBAAkB;;IAClE,QAAQ,CAAC,EAAE,YAAY;IAevB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAsHrB;IAEF,QAAQ,CAAC,cAAc,EAAE,cAAc,CA0HrC;gBAGA,OAAO,GAAE;QACP,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;QACrF,sEAAsE;QACtE,IAAI,CAAC,EAAE,MAAM,CAAC;KACV;IAYR,kEAAkE;IAClE,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,IAAI,OAAO,IAAI,0BAA0B,CAGxC;IAED,IAAI,oBAAoB,IAAI,0BAA0B,CAErD;IAED,IAAI,kBAAkB,IAAI,yBAAyB,CAKlD;IAuBD,UAAU,CAAC,EAAE,OAAO,EAAE,EAAE;QAAE,OAAO,EAAE,wBAAwB,CAAA;KAAE,GAAG,IAAI;IAUpE,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAkL3C,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,YAAY,EAAE;IAwBhD;;;;OAIG;IACG,qBAAqB,CAAC,KAAK,EAAE;QACjC,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC;IAkDlD;;;;OAIG;IACG,eAAe,CAAC,KAAK,EAAE;QAC3B,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAwC5C,YAAY,CAAC,EAAE,cAAc,EAAE,EAAE;QAAE,cAAc,EAAE,cAAc,CAAA;KAAE,GAAG,gBAAgB;IAIhF,gBAAgB,CAAC,EACrB,WAAW,EACX,cAAc,GACf,EAAE,UAAU,CAAC,WAAW,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrF,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAYhC,mCAAmC,CACvC,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC;IAoB5C,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA0BrE,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAa9D,cAAc,CAClB,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,MAAM,EAAE,CAAC;IASpB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,UAAU,CAAC,iBAAiB,CAAC,wBAAwB,CAAC,CAAC;CA6WzG"}
@@ -1,7 +1,8 @@
1
1
  import { createGithubSubscriptionTools, parseCreatedPullRequest, subscribeCurrentSessionToPullRequest } from "../../github/session-subscriptions.js";
2
+ import { attachGithubReconciler, attachGithubRules } from "../../github/rules.js";
3
+ import { attachGithubIssueReconciler } from "../../github/issue-reconciler.js";
2
4
  import { runGithubIssueTriage } from "../../github/issue-triage.js";
3
5
  import { buildGithubRoutes } from "../../github/routes.js";
4
- import { attachGithubReconciler, attachGithubRules } from "../../github/rules.js";
5
6
  import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
6
7
  import { PlatformGithubEventWorker } from "./event-worker.js";
7
8
  import { registerApiRoute } from "@mastra/core/server";
@@ -496,6 +497,7 @@ var PlatformGithubIntegration = class {
496
497
  storage: ctx.storage.generic,
497
498
  ingestFactoryEvent: attachGithubRules(this, ctx),
498
499
  reconcileFactoryState: this.#reconcileEnabled ? attachGithubReconciler(this, ctx, (input) => this.fetchPullRequestState(input)) : void 0,
500
+ reconcileIssuesFactoryState: this.#reconcileEnabled ? attachGithubIssueReconciler(this, ctx, (input) => this.fetchIssueState(input)) : void 0,
499
501
  pollEventsEnabled: this.#pollingEnabled,
500
502
  intervalMs: this.#pollingIntervalMs
501
503
  })];
@@ -518,9 +520,14 @@ var PlatformGithubIntegration = class {
518
520
  title: result.title ?? `PR ${input.number}`,
519
521
  url: result.html_url ?? `https://github.com/${input.repository}/pull/${input.number}`,
520
522
  state: result.state === "closed" ? "closed" : "open",
523
+ draft: result.draft === true,
521
524
  merged: result.merged === true,
525
+ assignees: (result.assignees ?? []).flatMap((assignee) => assignee.login ? [assignee.login] : []),
526
+ requestedReviewers: (result.requested_reviewers ?? []).flatMap((reviewer) => reviewer.login ? [reviewer.login] : []),
527
+ labels: (result.labels ?? []).flatMap((label) => typeof label === "string" ? [label] : label.name ? [label.name] : []),
522
528
  headBranch: result.head?.ref ?? "",
523
529
  baseBranch: result.base?.ref ?? "",
530
+ ...result.user?.login ? { author: result.user.login } : {},
524
531
  ...result.created_at ? { createdAt: result.created_at } : {},
525
532
  ...result.merged_by?.login ? { mergedBy: result.merged_by.login } : {}
526
533
  };
@@ -528,6 +535,36 @@ var PlatformGithubIntegration = class {
528
535
  return;
529
536
  }
530
537
  }
538
+ /**
539
+ * Reads live issue state through the Platform GitHub proxy for the
540
+ * reconciler. Returns undefined when the issue cannot be resolved (missing,
541
+ * is actually a PR, proxy error) so a sweep never fabricates a close.
542
+ */
543
+ async fetchIssueState(input) {
544
+ let repository;
545
+ try {
546
+ repository = splitRepository(input.repository);
547
+ } catch {
548
+ return;
549
+ }
550
+ try {
551
+ const result = await this.#client.request("GET", `${API_PREFIX}/github/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/issues/${input.number}`);
552
+ if (result.pull_request) return void 0;
553
+ return {
554
+ title: result.title ?? `Issue ${input.number}`,
555
+ url: result.html_url ?? `https://github.com/${input.repository}/issues/${input.number}`,
556
+ state: result.state === "closed" ? "closed" : "open",
557
+ ...result.state_reason ? { stateReason: result.state_reason } : {},
558
+ assignees: (result.assignees ?? []).flatMap((assignee) => assignee.login ? [assignee.login] : []),
559
+ labels: (result.labels ?? []).map((label) => typeof label === "string" ? label : label.name).filter((name) => Boolean(name)),
560
+ ...result.user?.login ? { author: result.user.login } : {},
561
+ ...result.created_at ? { createdAt: result.created_at } : {},
562
+ ...result.updated_at ? { updatedAt: result.updated_at } : {}
563
+ };
564
+ } catch {
565
+ return;
566
+ }
567
+ }
531
568
  sessionTools({ requestContext }) {
532
569
  return createGithubSubscriptionTools(requestContext, this);
533
570
  }
@@ -835,6 +872,7 @@ function parseIntakeIssue(sourceId, issue) {
835
872
  stateType: issue.state,
836
873
  priority: null,
837
874
  assignee: issue.assignees[0] ?? null,
875
+ assignees: issue.assignees,
838
876
  source: sourceId,
839
877
  labels: issue.labels,
840
878
  commentCount: issue.commentCount,
@@ -859,6 +897,9 @@ function parsePullRequest(pullRequest) {
859
897
  title: pullRequest.title,
860
898
  url: pullRequest.htmlUrl,
861
899
  author: pullRequest.user?.login ?? null,
900
+ assignees: pullRequest.assignees ?? [],
901
+ requestedReviewers: pullRequest.requestedReviewers ?? [],
902
+ labels: pullRequest.labels ?? [],
862
903
  body: pullRequest.body?.trim() ? pullRequest.body : null,
863
904
  state: pullRequest.state,
864
905
  draft: pullRequest.draft,