@mastra/factory 0.10.0-alpha.1 → 0.10.0-alpha.13

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 (138) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +2 -1
  4. package/dist/auth.js.map +1 -1
  5. package/dist/factory.d.ts +8 -0
  6. package/dist/factory.d.ts.map +1 -1
  7. package/dist/factory.js +25 -8
  8. package/dist/factory.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -1
  12. package/dist/integrations/base.d.ts +3 -1
  13. package/dist/integrations/base.d.ts.map +1 -1
  14. package/dist/integrations/github/integration.d.ts +14 -0
  15. package/dist/integrations/github/integration.d.ts.map +1 -1
  16. package/dist/integrations/github/integration.js +48 -1
  17. package/dist/integrations/github/integration.js.map +1 -1
  18. package/dist/integrations/github/provenance.d.ts +7 -2
  19. package/dist/integrations/github/provenance.d.ts.map +1 -1
  20. package/dist/integrations/github/provenance.js +16 -2
  21. package/dist/integrations/github/provenance.js.map +1 -1
  22. package/dist/integrations/github/routes.d.ts +8 -1
  23. package/dist/integrations/github/routes.d.ts.map +1 -1
  24. package/dist/integrations/github/routes.js +228 -7
  25. package/dist/integrations/github/routes.js.map +1 -1
  26. package/dist/integrations/github/rules.d.ts.map +1 -1
  27. package/dist/integrations/github/rules.js +193 -127
  28. package/dist/integrations/github/rules.js.map +1 -1
  29. package/dist/integrations/github/session-subscriptions.d.ts +9 -0
  30. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  31. package/dist/integrations/github/session-subscriptions.js +39 -1
  32. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  33. package/dist/integrations/linear/routes.d.ts.map +1 -1
  34. package/dist/integrations/linear/routes.js +50 -0
  35. package/dist/integrations/linear/routes.js.map +1 -1
  36. package/dist/integrations/platform/github/integration.d.ts +3 -1
  37. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  38. package/dist/integrations/platform/github/integration.js +33 -0
  39. package/dist/integrations/platform/github/integration.js.map +1 -1
  40. package/dist/routes/attention.d.ts +17 -0
  41. package/dist/routes/attention.d.ts.map +1 -0
  42. package/dist/routes/attention.js +305 -0
  43. package/dist/routes/attention.js.map +1 -0
  44. package/dist/routes/intake.d.ts +5 -0
  45. package/dist/routes/intake.d.ts.map +1 -1
  46. package/dist/routes/intake.js +70 -19
  47. package/dist/routes/intake.js.map +1 -1
  48. package/dist/routes/surface.d.ts +6 -5
  49. package/dist/routes/surface.d.ts.map +1 -1
  50. package/dist/routes/surface.js +62 -43
  51. package/dist/routes/surface.js.map +1 -1
  52. package/dist/routes/work-items.d.ts.map +1 -1
  53. package/dist/routes/work-items.js +35 -14
  54. package/dist/routes/work-items.js.map +1 -1
  55. package/dist/rules/defaults.js +5 -5
  56. package/dist/rules/defaults.js.map +1 -1
  57. package/dist/rules/dispatch-errors.d.ts +13 -0
  58. package/dist/rules/dispatch-errors.d.ts.map +1 -0
  59. package/dist/rules/dispatch-errors.js +77 -0
  60. package/dist/rules/dispatch-errors.js.map +1 -0
  61. package/dist/rules/dispatcher.d.ts +9 -0
  62. package/dist/rules/dispatcher.d.ts.map +1 -1
  63. package/dist/rules/dispatcher.js +118 -45
  64. package/dist/rules/dispatcher.js.map +1 -1
  65. package/dist/rules/processor.d.ts.map +1 -1
  66. package/dist/rules/processor.js +30 -14
  67. package/dist/rules/processor.js.map +1 -1
  68. package/dist/rules/terminal-cleanup.d.ts +1 -1
  69. package/dist/rules/terminal-cleanup.d.ts.map +1 -1
  70. package/dist/rules/terminal-cleanup.js +2 -2
  71. package/dist/rules/terminal-cleanup.js.map +1 -1
  72. package/dist/rules/tools.d.ts.map +1 -1
  73. package/dist/rules/tools.js +9 -5
  74. package/dist/rules/tools.js.map +1 -1
  75. package/dist/rules/transition-service.d.ts +5 -2
  76. package/dist/rules/transition-service.d.ts.map +1 -1
  77. package/dist/rules/transition-service.js +15 -2
  78. package/dist/rules/transition-service.js.map +1 -1
  79. package/dist/rules/types.d.ts +6 -1
  80. package/dist/rules/types.d.ts.map +1 -1
  81. package/dist/rules/types.js +25 -1
  82. package/dist/rules/types.js.map +1 -1
  83. package/dist/secret-encryption.d.ts +25 -0
  84. package/dist/secret-encryption.d.ts.map +1 -0
  85. package/dist/secret-encryption.js +96 -0
  86. package/dist/secret-encryption.js.map +1 -0
  87. package/dist/session/factory-session.d.ts +13 -3
  88. package/dist/session/factory-session.d.ts.map +1 -1
  89. package/dist/session/factory-session.js +15 -6
  90. package/dist/session/factory-session.js.map +1 -1
  91. package/dist/session/first-exec-capture.d.ts +16 -10
  92. package/dist/session/first-exec-capture.d.ts.map +1 -1
  93. package/dist/session/first-exec-capture.js +74 -14
  94. package/dist/session/first-exec-capture.js.map +1 -1
  95. package/dist/session/first-message-capture.d.ts +14 -10
  96. package/dist/session/first-message-capture.d.ts.map +1 -1
  97. package/dist/session/first-message-capture.js +17 -11
  98. package/dist/session/first-message-capture.js.map +1 -1
  99. package/dist/session/session-title.d.ts +7 -0
  100. package/dist/session/session-title.d.ts.map +1 -0
  101. package/dist/session/session-title.js +14 -0
  102. package/dist/session/session-title.js.map +1 -0
  103. package/dist/session/thread-title-mirror.d.ts +35 -0
  104. package/dist/session/thread-title-mirror.d.ts.map +1 -0
  105. package/dist/session/thread-title-mirror.js +51 -0
  106. package/dist/session/thread-title-mirror.js.map +1 -0
  107. package/dist/storage/domains/credentials/base.d.ts +3 -1
  108. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  109. package/dist/storage/domains/credentials/base.js +33 -15
  110. package/dist/storage/domains/credentials/base.js.map +1 -1
  111. package/dist/storage/domains/custom-providers/base.d.ts +3 -1
  112. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -1
  113. package/dist/storage/domains/custom-providers/base.js +45 -25
  114. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  115. package/dist/storage/domains/integrations/base.d.ts +3 -1
  116. package/dist/storage/domains/integrations/base.d.ts.map +1 -1
  117. package/dist/storage/domains/integrations/base.js +28 -12
  118. package/dist/storage/domains/integrations/base.js.map +1 -1
  119. package/dist/storage/domains/source-control/base.d.ts +8 -0
  120. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  121. package/dist/storage/domains/source-control/base.js +14 -2
  122. package/dist/storage/domains/source-control/base.js.map +1 -1
  123. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  124. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  125. package/dist/storage/domains/source-control/inmemory.js +7 -0
  126. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  127. package/dist/storage/domains/work-items/base.d.ts +120 -12
  128. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  129. package/dist/storage/domains/work-items/base.js +514 -44
  130. package/dist/storage/domains/work-items/base.js.map +1 -1
  131. package/dist/work-item-branch.d.ts +23 -0
  132. package/dist/work-item-branch.d.ts.map +1 -0
  133. package/dist/work-item-branch.js +39 -0
  134. package/dist/work-item-branch.js.map +1 -0
  135. package/factory-skills/factory-rereview/SKILL.md +2 -0
  136. package/factory-skills/factory-review/SKILL.md +2 -0
  137. package/factory-skills/factory-triage/SKILL.md +7 -17
  138. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":["#ingestProject","#relatedItem","#isFactoryLogin","#resolveItem"],"sources":["../../../src/integrations/github/rules.ts"],"sourcesContent":["import { resolveFactoryGithubRule } from '../../rules/resolve.js';\nimport type {\n FactoryGithubEventName,\n FactoryGithubRuleContext,\n FactoryRuleActor,\n FactoryRuleDecision,\n FactoryRules,\n} from '../../rules/types.js';\nimport { validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type {\n ExternalRepositoryProjectTarget,\n SourceControlStorageHandle,\n} from '../../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport type { GithubAppIdentity } from './app-identity.js';\nimport type { GithubRepositoryPermission } from './integration.js';\nimport { changeRequestTargetKey } from './subscriptions.js';\nimport type { ParsedGithubWebhook } from './webhook.js';\n\nconst TRUSTED_PERMISSIONS = new Set(['write', 'admin']);\nconst RULE_TIMEOUT_MS = 5_000;\nconst FACTORY_TRIAGE_COMMENT_MARKER = '<!-- mastra-factory-triage -->';\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\nfunction object(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction string(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction number(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction boolean(value: unknown): boolean | undefined {\n return typeof value === 'boolean' ? value : undefined;\n}\n\nfunction actorLogins(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap(actor => {\n const login = string(object(actor)?.login);\n return login ? [login] : [];\n });\n}\n\nfunction labelNames(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap(label => {\n if (typeof label === 'string') return label ? [label] : [];\n const name = string(object(label)?.name);\n return name ? [name] : [];\n });\n}\n\nfunction eventName(parsed: ParsedGithubWebhook): FactoryGithubEventName | undefined {\n const action = string(parsed.payload.action);\n if (parsed.event === 'issues' && action === 'opened') return 'issueOpened';\n if (parsed.event === 'issues' && action === 'edited') {\n const changes = object(parsed.payload.changes);\n return object(changes?.title) || object(changes?.body) ? 'issueEdited' : undefined;\n }\n if (parsed.event === 'issues' && action === 'closed') return 'issueClosed';\n if (parsed.event === 'issue_comment') {\n const issue = object(parsed.payload.issue);\n // A comment on a PR arrives as `issue_comment` with `issue.pull_request` set.\n // It routes to the PR's own event so it binds to the authoring Work item via\n // provenance instead of being mistaken for a comment on an issue of the same\n // number. Only `created` matters: edits and deletions of an existing comment\n // are not new feedback to act on.\n if (object(issue?.pull_request)) {\n return action === 'created' ? 'pullRequestCommentCreated' : undefined;\n }\n if (action === 'created') return 'issueCommentCreated';\n if (action === 'edited') return 'issueCommentEdited';\n if (action === 'deleted') return 'issueCommentDeleted';\n }\n if (parsed.event === 'pull_request' && action === 'opened') return 'pullRequestOpened';\n if (parsed.event === 'pull_request' && action === 'synchronize') return 'pullRequestUpdated';\n if (parsed.event === 'pull_request' && action === 'closed') {\n return boolean(object(parsed.payload.pull_request)?.merged) ? 'pullRequestMerged' : 'pullRequestClosed';\n }\n if (parsed.event === 'pull_request' && action === 'review_requested') return 'pullRequestReviewRequested';\n if (parsed.event === 'pull_request_review' && action === 'submitted') return 'pullRequestReviewSubmitted';\n return undefined;\n}\n\n/**\n * Canonical source keys (`github-issue:N`, `github-pr:N`) do not identify a\n * repository, so a project linked to several repositories could bind repo A's\n * event to repo B's same-numbered card. The card's intake-stamped URL is\n * authoritative; the intake-stamped `githubRepositoryId` covers URL-less\n * cards. A card with neither signal cannot be attributed by number alone.\n */\nfunction cardBelongsToRepository(item: WorkItemRow, repositoryId: number, repositoryFullName: string): boolean {\n const url = item.externalSource?.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/(?:issues|pull)\\/\\d+(?:[/?#]|$)/.exec(url);\n if (match && match[1] === repositoryFullName) return true;\n }\n // A renamed repository leaves the old owner/name in the card URL, so a URL\n // mismatch still defers to the stable intake-stamped repository id.\n return item.metadata?.githubRepositoryId === repositoryId;\n}\n\nfunction canonicalSourceKey(kind: 'issue' | 'pull-request', itemNumber: number): string {\n return kind === 'issue' ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;\n}\n\nfunction legacySourceKey(repositoryId: number, kind: 'issue' | 'pull-request', itemNumber: number): string {\n return `github:${repositoryId}:${kind}:${itemNumber}`;\n}\n\nfunction provenanceTarget(repositoryId: number, pullRequestNumber: number): string {\n return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;\n}\n\nfunction workItemSource(item: WorkItemRow) {\n if (!item.externalSource) return 'manual' as const;\n return item.externalSource.type === 'pull-request' ? ('github-pr' as const) : ('github-issue' as const);\n}\n\nfunction workItemSourceKey(item: WorkItemRow): string | null {\n return item.externalSource?.externalId ?? null;\n}\n\nasync function githubActor(\n github: GithubRulesIntegration,\n input: { installationId: number; repository: string; login: string; factoryAuthored: boolean },\n): Promise<FactoryRuleActor> {\n let trusted = false;\n try {\n const permission = await github.getRepositoryCollaboratorPermission(\n input.installationId,\n input.repository,\n input.login,\n );\n trusted = permission !== undefined && TRUSTED_PERMISSIONS.has(permission);\n } catch {\n trusted = false;\n }\n return { type: 'github', login: input.login, trusted, factoryAuthored: input.factoryAuthored };\n}\n\ninterface FactoryPullRequestProvenanceData {\n kind: 'factory-pr-provenance';\n workItemId: string;\n}\n\nfunction pullRequestProvenance(data: Record<string, unknown> | undefined): FactoryPullRequestProvenanceData | null {\n if (!data || data.kind !== 'factory-pr-provenance' || typeof data.workItemId !== 'string') return null;\n return { kind: 'factory-pr-provenance', workItemId: data.workItemId };\n}\n\nexport interface GithubRulesIntegration {\n readonly slug?: string;\n /**\n * Factory's own GitHub login, used to ignore its own writes. Optional because\n * not every integration can name itself; when absent, self-recognition falls\n * back to the configured slug and, failing that, to content Factory stamps\n * itself (see `FACTORY_TRIAGE_COMMENT_MARKER`).\n */\n readonly identity?: GithubAppIdentity;\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface GithubRulesOptions {\n github: GithubRulesIntegration;\n sourceControl: SourceControlStorageHandle;\n /** Integration-scoped storage; provenance rows are validated at read. */\n integrationStorage: IntegrationStorageHandle;\n projects: FactoryProjectsStorage;\n storage: WorkItemsStorage;\n rules: FactoryRules;\n}\n\nexport class GithubRules {\n constructor(private readonly options: GithubRulesOptions) {}\n\n /**\n * Whether a login is Factory itself. Prefers the resolved identity, which is\n * observed from Factory's own writes, and falls back to the configured slug.\n * An unset slug must not silently answer \"not Factory\" — that is what\n * disabled every self-loop guard.\n */\n #isFactoryLogin(login: string | undefined): boolean {\n const identity = this.options.github.identity;\n if (identity?.known) return identity.matches(login);\n const slug = this.options.github.slug?.trim();\n if (!slug || !login) return false;\n return login.toLowerCase() === `${slug.toLowerCase()}[bot]`;\n }\n\n\n async ingest(parsed: ParsedGithubWebhook): Promise<{ status: 'ignored' | 'committed' | 'replayed' | 'missing' }> {\n const event = eventName(parsed);\n const repository = object(parsed.payload.repository);\n const installationId = number(object(parsed.payload.installation)?.id);\n const repositoryId = number(repository?.id);\n const repositoryName = string(repository?.full_name);\n const login = string(object(parsed.payload.sender)?.login);\n if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: 'ignored' };\n\n const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({\n installationExternalId: String(installationId),\n repositoryExternalId: String(repositoryId),\n });\n if (projects.length === 0) return { status: 'ignored' };\n const results = [];\n for (const project of projects) {\n results.push(\n await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project),\n );\n }\n if (results.some(result => result.status === 'committed')) return { status: 'committed' };\n if (results.some(result => result.status === 'replayed')) return { status: 'replayed' };\n return results[0] ?? { status: 'ignored' };\n }\n\n async #ingestProject(\n parsed: ParsedGithubWebhook,\n event: FactoryGithubEventName,\n installationId: number,\n repositoryId: number,\n repositoryName: string,\n login: string,\n project: ExternalRepositoryProjectTarget,\n ): Promise<{ status: 'ignored' | 'committed' | 'replayed' | 'missing' }> {\n const factoryProject = await this.options.projects.get({\n orgId: project.orgId,\n id: project.factoryProjectId,\n });\n if (!factoryProject) return { status: 'missing' };\n const issue = object(parsed.payload.issue);\n const issueComment = object(parsed.payload.comment);\n const changes = object(parsed.payload.changes);\n // A comment on a PR carries the PR under `issue` (with `pull_request` set)\n // and has no `pull_request` payload of its own. Read the PR from `issue` in\n // that case so provenance and `context.pullRequest` behave as they do for\n // every other PR event, and so the number is never treated as an issue's.\n const commentOnPullRequest = object(parsed.payload.issue)?.pull_request !== undefined;\n const pullRequest = object(parsed.payload.pull_request) ?? (commentOnPullRequest ? issue : undefined);\n const issueNumber = commentOnPullRequest ? undefined : number(issue?.number);\n const pullRequestNumber = number(pullRequest?.number);\n const provenance = pullRequestNumber\n ? pullRequestProvenance(\n (\n await this.options.integrationStorage.subscriptions.listByTarget(\n provenanceTarget(repositoryId, pullRequestNumber),\n { status: 'active' },\n )\n ).find(subscription => subscription.orgId === project.orgId)?.data,\n )\n : null;\n // Re-review events target the PR's own Review card, not the Work item that\n // provenance would otherwise bind the event to. For review_requested the\n // sender is whoever clicked re-request, so a Factory-authored PR must not\n // brand a human requester as factory-authored.\n const reviewRequested = event === 'pullRequestReviewRequested';\n // Provenance proves the *pull request* came from Factory, which is not the\n // same as the sender of this event. For events where the sender is whoever\n // reacted to the PR — re-requesting review, commenting, submitting a review\n // — branding them from provenance would mark every human and every review\n // bot as Factory. Only the app login identifies Factory for those.\n const senderIsResponder =\n reviewRequested || event === 'pullRequestCommentCreated' || event === 'pullRequestReviewSubmitted';\n const reReviewEvent = reviewRequested || event === 'pullRequestUpdated';\n const requestedReviewer = string(object(parsed.payload.requested_reviewer)?.login);\n const relatedItem = await this.#relatedItem(\n project.orgId,\n project.factoryProjectId,\n repositoryId,\n repositoryName,\n issueNumber,\n pullRequestNumber,\n string(object(pullRequest?.head)?.ref),\n reReviewEvent ? null : provenance,\n senderIsResponder && !reviewRequested,\n );\n const actor = await githubActor(this.options.github, {\n installationId,\n repository: repositoryName,\n login,\n factoryAuthored: (!senderIsResponder && provenance !== null) || this.#isFactoryLogin(login),\n });\n // A marked handoff comment is ignored only when Factory authored it: a\n // human may quote the marker to add an investigation lead, and that must\n // still retrigger triage. Recognising the author is therefore the whole\n // guard — when identity cannot be resolved this fails open and Factory's\n // own handoff cancels the run that wrote it.\n if (\n actor.type === 'github' &&\n actor.factoryAuthored &&\n (event === 'issueCommentCreated' || event === 'issueCommentEdited') &&\n string(issueComment?.body)?.includes(FACTORY_TRIAGE_COMMENT_MARKER)\n ) {\n return { status: 'ignored' };\n }\n const context: FactoryGithubRuleContext = {\n tenant: { orgId: project.orgId, projectId: project.factoryProjectId },\n actor,\n ingress: { type: 'github', id: `${installationId}:${parsed.deliveryId}` },\n cause: `github.${event}`,\n causalChain: [],\n ruleSetVersion: this.options.rules.version,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: workItemSource(relatedItem),\n sourceKey: workItemSourceKey(relatedItem),\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n metadata: relatedItem.metadata,\n },\n board: relatedItem.externalSource?.type === 'pull-request' ? ('review' as const) : ('work' as const),\n itemRevision: relatedItem.revision,\n }\n : {}),\n event,\n deliveryId: parsed.deliveryId,\n factory: { createdAt: factoryProject.createdAt.toISOString() },\n repository: { id: repositoryId, fullName: repositoryName },\n ...(issueNumber && string(issue?.title) && string(issue?.html_url)\n ? {\n issue: {\n number: issueNumber,\n title: string(issue?.title)!,\n url: string(issue?.html_url)!,\n ...(string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}),\n ...(string(issue?.updated_at) ? { updatedAt: string(issue?.updated_at) } : {}),\n assignees: actorLogins(issue?.assignees),\n labels: labelNames(issue?.labels),\n ...(string(issue?.state) === 'closed' || string(issue?.state) === 'open'\n ? { state: string(issue?.state) as 'open' | 'closed' }\n : {}),\n ...(string(issue?.state_reason) ? { stateReason: string(issue?.state_reason) } : {}),\n },\n }\n : {}),\n ...(event === 'issueEdited'\n ? { issueChange: { title: Boolean(object(changes?.title)), body: Boolean(object(changes?.body)) } }\n : {}),\n ...(number(issueComment?.id)\n ? {\n issueComment: {\n id: number(issueComment?.id)!,\n ...(string(issueComment?.body) ? { body: string(issueComment?.body) } : {}),\n ...(string(issueComment?.html_url) ? { url: string(issueComment?.html_url) } : {}),\n ...(string(object(issueComment?.user)?.login) ? { author: string(object(issueComment?.user)?.login) } : {}),\n ...(string(object(issueComment?.user)?.type) ? { authorType: string(object(issueComment?.user)?.type) } : {}),\n ...(string(issueComment?.created_at) ? { createdAt: string(issueComment?.created_at) } : {}),\n ...(string(issueComment?.updated_at) ? { updatedAt: string(issueComment?.updated_at) } : {}),\n },\n }\n : {}),\n ...(pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url)\n ? {\n pullRequest: {\n number: pullRequestNumber,\n title: string(pullRequest?.title)!,\n url: string(pullRequest?.html_url)!,\n ...(string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {}),\n state: string(pullRequest?.state) === 'closed' ? ('closed' as const) : ('open' as const),\n draft: boolean(pullRequest?.draft) ?? false,\n merged: boolean(pullRequest?.merged) ?? false,\n assignees: actorLogins(pullRequest?.assignees),\n requestedReviewers: actorLogins(pullRequest?.requested_reviewers),\n labels: labelNames(pullRequest?.labels),\n headBranch: string(object(pullRequest?.head)?.ref) ?? '',\n baseBranch: string(object(pullRequest?.base)?.ref) ?? '',\n },\n }\n : {}),\n ...(reviewRequested && requestedReviewer\n ? {\n reviewRequest: {\n reviewer: requestedReviewer,\n factoryReviewer: this.#isFactoryLogin(requestedReviewer),\n },\n }\n : {}),\n ...(object(parsed.payload.review)\n ? {\n review: {\n id: number(object(parsed.payload.review)?.id) ?? 0,\n state: string(object(parsed.payload.review)?.state) ?? 'unknown',\n url: string(object(parsed.payload.review)?.html_url) ?? '',\n },\n }\n : {}),\n };\n\n const rule = resolveFactoryGithubRule(this.options.rules, 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 GitHub rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: project.orgId,\n factoryProjectId: project.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: `${installationId}:${parsed.deliveryId}`, triggerType: `github.${event}` },\n ruleSetVersion: this.options.rules.version,\n expectedRevision: relatedItem?.revision ?? null,\n actor: { ...actor },\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n return { status: committed.status };\n }\n\n async #relatedItem(\n orgId: string,\n projectId: string,\n repositoryId: number,\n repositoryFullName: string,\n issueNumber: number | undefined,\n pullRequestNumber: number | undefined,\n pullRequestHeadBranch: string | undefined,\n provenance: FactoryPullRequestProvenanceData | null,\n preferAuthoringItem = false,\n ): Promise<WorkItemRow | undefined> {\n const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });\n const resolved = this.#resolveItem(\n items,\n repositoryId,\n repositoryFullName,\n issueNumber,\n pullRequestNumber,\n pullRequestHeadBranch,\n provenance,\n );\n // Feedback on a pull request has to reach the item that *wrote* the code.\n // Provenance normally lands it there directly, but when provenance is\n // missing the PR-number lookup wins and returns the PR's own Review card\n // instead — a board the feedback rules deliberately refuse to act on, so\n // the wake is silently dropped. The linked card records its author in\n // `parentWorkItemId`, so follow that link back rather than relaxing the\n // guard, which would let a Review card react to its own posted review.\n if (preferAuthoringItem && resolved?.externalSource?.type === 'pull-request' && resolved.parentWorkItemId) {\n return items.find(item => item.id === resolved.parentWorkItemId) ?? resolved;\n }\n return resolved;\n }\n\n #resolveItem(\n items: WorkItemRow[],\n repositoryId: number,\n repositoryFullName: string,\n issueNumber: number | undefined,\n pullRequestNumber: number | undefined,\n pullRequestHeadBranch: string | undefined,\n provenance: FactoryPullRequestProvenanceData | null,\n ): WorkItemRow | undefined {\n if (provenance) return items.find(item => item.id === provenance.workItemId);\n if (issueNumber) {\n return (\n items.find(\n item =>\n item.externalSource?.externalId === canonicalSourceKey('issue', issueNumber) &&\n cardBelongsToRepository(item, repositoryId, repositoryFullName),\n ) ?? items.find(item => item.externalSource?.externalId === legacySourceKey(repositoryId, 'issue', issueNumber))\n );\n }\n if (pullRequestNumber) {\n return (\n items.find(\n item =>\n item.externalSource?.externalId === canonicalSourceKey('pull-request', pullRequestNumber) &&\n cardBelongsToRepository(item, repositoryId, repositoryFullName),\n ) ??\n items.find(\n item => item.externalSource?.externalId === legacySourceKey(repositoryId, 'pull-request', pullRequestNumber),\n ) ??\n // Provenance fallback: a PR pushed from a work item's session branch\n // belongs to that item even when no gh-pr-create provenance was\n // recorded (session predating state seeding, or the PR was opened\n // outside the tracked tool call). Session branches are per-item\n // (`factory/issue-N`), so a head-branch match is unambiguous.\n (pullRequestHeadBranch\n ? items.find(\n item =>\n item.externalSource?.type !== 'pull-request' &&\n Object.values(item.sessions).some(session => session.branch === pullRequestHeadBranch),\n )\n : undefined)\n );\n }\n return undefined;\n }\n}\n\nexport interface ReconcilePullRequestState {\n title: string;\n url: string;\n state: 'open' | 'closed';\n draft: boolean;\n merged: boolean;\n assignees?: string[];\n requestedReviewers?: string[];\n labels?: string[];\n headBranch: string;\n baseBranch: string;\n author?: string;\n createdAt?: string;\n mergedBy?: string;\n}\n\nexport type GithubPullRequestFetcher = (input: {\n installationId: number;\n repository: string;\n number: number;\n}) => Promise<ReconcilePullRequestState | undefined>;\n\nexport interface ReconcileIssueState {\n title: string;\n url: string;\n state: 'open' | 'closed';\n /** GitHub close reason: `completed`, `not_planned`, or `duplicate`. */\n stateReason?: string;\n assignees?: string[];\n labels?: string[];\n author?: string;\n createdAt?: string;\n updatedAt?: string;\n}\n\nexport type GithubIssueFetcher = (input: {\n installationId: number;\n repository: string;\n number: number;\n}) => Promise<ReconcileIssueState | undefined>;\n\nexport interface ReconcileRepository {\n id: number;\n fullName: string;\n installationId: number;\n}\n\nexport interface ReconcileSweepSummary {\n /** Factory-configured repositories included in the sweep. */\n repositories: number;\n /** PRs whose live state was fetched from GitHub. */\n checked: number;\n /** Missed merges replayed through the rules ingress. */\n merged: number;\n /** Missed closes-without-merge replayed through the rules ingress. */\n closed: number;\n /** PRs/issues (or whole repositories) skipped because of an error. */\n failed: number;\n /** Error samples with context, capped at {@link RECONCILE_ERROR_SAMPLE_LIMIT}. */\n errors: Array<{ repository: string; pullRequestNumber?: number; issueNumber?: number; error: string }>;\n}\n\nexport type GithubPullRequestReconciler = (repositories: ReconcileRepository[]) => Promise<ReconcileSweepSummary>;\n\nexport const RECONCILE_ERROR_SAMPLE_LIMIT = 5;\n\nexport function sameStrings(left: unknown, right: string[] | undefined): boolean {\n if (right === undefined) return true;\n if (!Array.isArray(left)) return false;\n const leftValues = new Set(left.flatMap(value => (typeof value === 'string' ? [value] : [])));\n const rightValues = new Set(right);\n return leftValues.size === rightValues.size && [...leftValues].every(value => rightValues.has(value));\n}\n\n/**\n * Extracts the PR number a work item tracks, but only when the item belongs\n * to the given repository. Card URLs pin the repository unambiguously; the\n * legacy source key embeds the repository id. Canonical keys (`github-pr:N`)\n * carry no repository, so they are only trusted when the item has no URL —\n * a project mapped to multiple repositories must not reconcile one repo's\n * card against another repo's PR number.\n */\nfunction reconcilablePullRequestNumber(item: WorkItemRow, repository: ReconcileRepository): number | undefined {\n if (item.externalSource?.type !== 'pull-request') return undefined;\n const url = item.externalSource.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/pull\\/(\\d+)(?:[/?#]|$)/.exec(url);\n if (!match) return undefined;\n return match[1] === repository.fullName ? Number(match[2]) : undefined;\n }\n const externalId = item.externalSource.externalId;\n const legacy = /^github:(\\d+):pull-request:(\\d+)$/.exec(externalId);\n if (legacy) return Number(legacy[1]) === repository.id ? Number(legacy[2]) : undefined;\n const canonical = /^github-pr:(\\d+)$/.exec(externalId);\n return canonical ? Number(canonical[1]) : undefined;\n}\n\n/**\n * Extracts the issue number a work item tracks, but only when the item\n * belongs to the given repository. Stricter than\n * {@link reconcilablePullRequestNumber}: a canonical key with no URL is only\n * trusted when the intake-stamped `githubRepositoryId` confirms the\n * repository, because the sweep initiates closes on its own.\n */\nexport function reconcilableIssueNumber(item: WorkItemRow, repository: ReconcileRepository): number | undefined {\n if (item.externalSource?.type !== 'issue') return undefined;\n const url = item.externalSource.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/issues\\/(\\d+)(?:[/?#]|$)/.exec(url);\n if (!match) return undefined;\n // A renamed repository leaves the old owner/name in the card URL, so a\n // URL mismatch still defers to the stable intake-stamped repository id.\n const belongs = match[1] === repository.fullName || item.metadata?.githubRepositoryId === repository.id;\n return belongs ? Number(match[2]) : undefined;\n }\n const externalId = item.externalSource.externalId;\n const legacy = /^github:(\\d+):issue:(\\d+)$/.exec(externalId);\n if (legacy) return Number(legacy[1]) === repository.id ? Number(legacy[2]) : undefined;\n const canonical = /^github-issue:(\\d+)$/.exec(externalId);\n if (!canonical) return undefined;\n // Canonical keys carry no repository; only the intake-stamped repository id\n // can attribute a URL-less card, and guessing would let a multi-repo\n // project close repo B's card because repo A's same-numbered issue closed.\n return item.metadata?.githubRepositoryId === repository.id ? Number(canonical[1]) : undefined;\n}\n\nexport function reconciledIssueClosedEvent(\n repository: ReconcileRepository,\n issueNumber: number,\n state: ReconcileIssueState,\n): ParsedGithubWebhook {\n return {\n event: 'issues',\n // Stable per (repository, issue): the ingress dedupe makes repeat\n // reconcile cycles replay instead of re-committing decisions.\n deliveryId: `reconcile:${repository.id}:issue:${issueNumber}:closed`,\n payload: {\n action: 'closed',\n installation: { id: repository.installationId },\n repository: { id: repository.id, full_name: repository.fullName },\n sender: { login: state.author ?? 'github' },\n issue: {\n number: issueNumber,\n title: state.title,\n html_url: state.url,\n state: 'closed',\n ...(state.stateReason ? { state_reason: state.stateReason } : {}),\n ...(state.createdAt ? { created_at: state.createdAt } : {}),\n ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),\n assignees: (state.assignees ?? []).map(login => ({ login })),\n },\n },\n };\n}\n\nexport function reconciledClosedEvent(\n repository: ReconcileRepository,\n pullRequestNumber: number,\n state: ReconcilePullRequestState,\n): ParsedGithubWebhook {\n return {\n event: 'pull_request',\n // Stable per (repository, PR, outcome): the ingress dedupe makes repeat\n // reconcile cycles replay instead of re-committing decisions.\n deliveryId: `reconcile:${repository.id}:pull-request:${pullRequestNumber}:${state.merged ? 'merged' : 'closed'}`,\n payload: {\n action: 'closed',\n installation: { id: repository.installationId },\n repository: { id: repository.id, full_name: repository.fullName },\n sender: { login: state.mergedBy ?? 'github' },\n pull_request: {\n number: pullRequestNumber,\n title: state.title,\n html_url: state.url,\n ...(state.createdAt ? { created_at: state.createdAt } : {}),\n state: 'closed',\n draft: state.draft,\n merged: state.merged,\n assignees: (state.assignees ?? []).map(login => ({ login })),\n requested_reviewers: (state.requestedReviewers ?? []).map(login => ({ login })),\n labels: (state.labels ?? []).map(name => ({ name })),\n head: { ref: state.headBranch },\n base: { ref: state.baseBranch },\n },\n },\n };\n}\n\n/**\n * The webhook handler retires subscriptions itself; the sweep replays only the\n * rules ingress, so without this the thread's PR chip and the workspace row\n * stay `open` forever on a deployment GitHub cannot reach.\n */\nasync function retireReconciledSubscriptions(\n storage: IntegrationStorageHandle,\n repository: ReconcileRepository,\n pullRequestNumber: number,\n merged: boolean,\n): Promise<void> {\n const target = changeRequestTargetKey({\n installationExternalId: String(repository.installationId),\n repositoryExternalId: String(repository.id),\n changeRequestId: String(pullRequestNumber),\n });\n const rows = await storage.subscriptions.listByTarget(target);\n await Promise.all(\n rows\n .filter(row => row.status === 'open')\n .map(row => storage.subscriptions.updateStatus(row.id, merged ? 'merged' : 'closed')),\n );\n}\n\n/**\n * State-based safety net for merge signals: webhooks and event-log tailing\n * can miss a merge (cursor gaps, downtime, terminally failed decisions), so\n * this sweep compares still-open PR cards against actual GitHub state and\n * replays the merge through the normal rules ingress when they disagree.\n */\nexport function createGithubPullRequestReconciler(\n options: GithubRulesOptions,\n fetchPullRequest: GithubPullRequestFetcher,\n): GithubPullRequestReconciler {\n const rules = new GithubRules(options);\n return async repositories => {\n const summary: ReconcileSweepSummary = {\n repositories: 0,\n checked: 0,\n merged: 0,\n closed: 0,\n failed: 0,\n errors: [],\n };\n const recordFailure = (\n repository: ReconcileRepository,\n error: unknown,\n pullRequestNumber?: number,\n ) => {\n summary.failed += 1;\n if (summary.errors.length < RECONCILE_ERROR_SAMPLE_LIMIT) {\n summary.errors.push({\n repository: repository.fullName,\n ...(pullRequestNumber === undefined ? {} : { pullRequestNumber }),\n error: error instanceof Error ? error.message : String(error),\n });\n }\n };\n // An installation can expose hundreds of repositories; only the ones\n // actually linked to a factory project can have cards to reconcile, so\n // scope the sweep to those up front instead of probing each repository.\n const configured = new Set(\n (await options.sourceControl.projectRepositories.listConfiguredExternalKeys()).map(\n key => `${key.installationExternalId}\\u0000${key.repositoryExternalId}`,\n ),\n );\n const scoped = repositories.filter(repository =>\n configured.has(`${repository.installationId}\\u0000${repository.id}`),\n );\n summary.repositories = scoped.length;\n for (const repository of scoped) {\n // One broken repository (or a failing token exchange for its\n // installation) must not abort the sweep for the others.\n let cardsByNumber: Map<number, WorkItemRow[]>;\n try {\n const projects = await options.sourceControl.projectRepositories.listByExternalRepository({\n installationExternalId: String(repository.installationId),\n repositoryExternalId: String(repository.id),\n });\n if (projects.length === 0) continue;\n cardsByNumber = new Map<number, WorkItemRow[]>();\n for (const project of projects) {\n const items = await options.storage.list({\n orgId: project.orgId,\n factoryProjectId: project.factoryProjectId,\n });\n for (const item of items) {\n const stage = item.stages[0];\n const pullRequestNumber = reconcilablePullRequestNumber(item, repository);\n if (!pullRequestNumber) continue;\n const metadata = item.metadata ?? {};\n // An approving review ends the card at `done` while its pull request is\n // still open, so only a closed pull request retires a card from the sweep.\n const pullRequestSettled =\n metadata.state === 'closed' &&\n typeof metadata.draft === 'boolean' &&\n typeof metadata.merged === 'boolean' &&\n typeof metadata.author === 'string' &&\n Array.isArray(metadata.assignees) &&\n Array.isArray(metadata.requestedReviewers) &&\n Array.isArray(metadata.labels);\n if ((stage === 'done' || stage === 'canceled') && pullRequestSettled) continue;\n const cards = cardsByNumber.get(pullRequestNumber) ?? [];\n cards.push(item);\n cardsByNumber.set(pullRequestNumber, cards);\n }\n }\n } catch (error) {\n recordFailure(repository, error);\n continue;\n }\n for (const [pullRequestNumber, cards] of cardsByNumber) {\n try {\n const state = await fetchPullRequest({\n installationId: repository.installationId,\n repository: repository.fullName,\n number: pullRequestNumber,\n });\n summary.checked += 1;\n if (!state) continue;\n for (const card of cards) {\n const metadata = card.metadata ?? {};\n const statusChanged =\n metadata.state !== state.state || metadata.draft !== state.draft || metadata.merged !== state.merged;\n const authorChanged = state.author !== undefined && metadata.author !== state.author;\n const assigneesChanged = !sameStrings(metadata.assignees, state.assignees);\n const reviewersChanged = !sameStrings(metadata.requestedReviewers, state.requestedReviewers);\n const labelsChanged = !sameStrings(metadata.labels, state.labels);\n if (!statusChanged && !authorChanged && !assigneesChanged && !reviewersChanged && !labelsChanged) continue;\n try {\n await options.storage.update({\n orgId: card.orgId,\n id: card.id,\n userId: 'factory-rule-dispatcher',\n patch: {\n metadata: {\n state: state.state,\n draft: state.draft,\n merged: state.merged,\n ...(state.author ? { author: state.author } : {}),\n ...(state.assignees ? { assignees: state.assignees } : {}),\n ...(state.requestedReviewers ? { requestedReviewers: state.requestedReviewers } : {}),\n ...(state.labels ? { labels: state.labels } : {}),\n },\n },\n });\n } catch (error) {\n recordFailure(repository, error, pullRequestNumber);\n }\n }\n if (state.state !== 'closed') continue;\n await rules.ingest(reconciledClosedEvent(repository, pullRequestNumber, state));\n await retireReconciledSubscriptions(options.integrationStorage, repository, pullRequestNumber, state.merged);\n if (state.merged) summary.merged += 1;\n else summary.closed += 1;\n } catch (error) {\n recordFailure(repository, error, pullRequestNumber);\n }\n }\n }\n return summary;\n };\n}\n\nexport function githubRulesOptions(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n): GithubRulesOptions | undefined {\n if (!context.rules) return undefined;\n return {\n github,\n sourceControl: context.storage.sourceControl,\n integrationStorage: context.storage.generic,\n projects: context.storage.projects,\n storage: context.rules.workItems,\n rules: context.rules.config,\n };\n}\n\nexport function attachGithubRules(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n): ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined {\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n const rules = new GithubRules(options);\n return event => rules.ingest(event);\n}\n\nexport function attachGithubReconciler(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n fetchPullRequest: GithubPullRequestFetcher,\n): GithubPullRequestReconciler | undefined {\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n return createGithubPullRequestReconciler(options, fetchPullRequest);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","names":["#ingestProject","#relatedItem","#isFactoryLogin","#linkedClosureItem","#resolveItem"],"sources":["../../../src/integrations/github/rules.ts"],"sourcesContent":["import { resolveFactoryGithubRule } from '../../rules/resolve.js';\nimport type {\n FactoryGithubEventName,\n FactoryGithubRuleContext,\n FactoryRuleActor,\n FactoryRuleDecision,\n FactoryRules,\n} from '../../rules/types.js';\nimport { isTerminalFactoryRuleStage } from '../../rules/types.js';\nimport { validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type {\n ExternalRepositoryProjectTarget,\n SourceControlStorageHandle,\n} from '../../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport { FACTORY_PULL_REQUEST_RECONCILIATION_KEY } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport type { GithubAppIdentity } from './app-identity.js';\nimport type { GithubRepositoryPermission } from './integration.js';\nimport { changeRequestTargetKey } from './subscriptions.js';\nimport type { ParsedGithubWebhook } from './webhook.js';\n\nconst TRUSTED_PERMISSIONS = new Set(['write', 'admin']);\nconst RULE_TIMEOUT_MS = 5_000;\nconst FACTORY_TRIAGE_COMMENT_MARKER = '<!-- mastra-factory-triage -->';\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\nfunction object(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction string(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction number(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction boolean(value: unknown): boolean | undefined {\n return typeof value === 'boolean' ? value : undefined;\n}\n\nfunction actorLogins(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap(actor => {\n const login = string(object(actor)?.login);\n return login ? [login] : [];\n });\n}\n\nfunction labelNames(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap(label => {\n if (typeof label === 'string') return label ? [label] : [];\n const name = string(object(label)?.name);\n return name ? [name] : [];\n });\n}\n\nfunction eventName(parsed: ParsedGithubWebhook): FactoryGithubEventName | undefined {\n const action = string(parsed.payload.action);\n if (parsed.event === 'issues' && action === 'opened') return 'issueOpened';\n if (parsed.event === 'issues' && action === 'edited') {\n const changes = object(parsed.payload.changes);\n return object(changes?.title) || object(changes?.body) ? 'issueEdited' : undefined;\n }\n if (parsed.event === 'issues' && action === 'closed') return 'issueClosed';\n if (parsed.event === 'issue_comment') {\n const issue = object(parsed.payload.issue);\n // A comment on a PR arrives as `issue_comment` with `issue.pull_request` set.\n // It routes to the PR's own event so it binds to the authoring Work item via\n // provenance instead of being mistaken for a comment on an issue of the same\n // number. Only `created` matters: edits and deletions of an existing comment\n // are not new feedback to act on.\n if (object(issue?.pull_request)) {\n return action === 'created' ? 'pullRequestCommentCreated' : undefined;\n }\n if (action === 'created') return 'issueCommentCreated';\n if (action === 'edited') return 'issueCommentEdited';\n if (action === 'deleted') return 'issueCommentDeleted';\n }\n if (parsed.event === 'pull_request' && action === 'opened') return 'pullRequestOpened';\n if (parsed.event === 'pull_request' && action === 'synchronize') return 'pullRequestUpdated';\n if (parsed.event === 'pull_request' && action === 'closed') {\n return boolean(object(parsed.payload.pull_request)?.merged) ? 'pullRequestMerged' : 'pullRequestClosed';\n }\n if (parsed.event === 'pull_request' && action === 'review_requested') return 'pullRequestReviewRequested';\n if (parsed.event === 'pull_request_review' && action === 'submitted') return 'pullRequestReviewSubmitted';\n return undefined;\n}\n\n/**\n * Canonical source keys (`github-issue:N`, `github-pr:N`) do not identify a\n * repository, so a project linked to several repositories could bind repo A's\n * event to repo B's same-numbered card. The card's intake-stamped URL is\n * authoritative; the intake-stamped `githubRepositoryId` covers URL-less\n * cards. A card with neither signal cannot be attributed by number alone.\n */\nfunction cardBelongsToRepository(item: WorkItemRow, repositoryId: number, repositoryFullName: string): boolean {\n const url = item.externalSource?.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/(?:issues|pull)\\/\\d+(?:[/?#]|$)/.exec(url);\n if (match && match[1] === repositoryFullName) return true;\n }\n // A renamed repository leaves the old owner/name in the card URL, so a URL\n // mismatch still defers to the stable intake-stamped repository id.\n return item.metadata?.githubRepositoryId === repositoryId;\n}\n\nfunction canonicalSourceKey(kind: 'issue' | 'pull-request', itemNumber: number): string {\n return kind === 'issue' ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;\n}\n\nfunction legacySourceKey(repositoryId: number, kind: 'issue' | 'pull-request', itemNumber: number): string {\n return `github:${repositoryId}:${kind}:${itemNumber}`;\n}\n\nfunction provenanceTarget(repositoryId: number, pullRequestNumber: number): string {\n return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;\n}\n\nfunction workItemSource(item: WorkItemRow) {\n if (!item.externalSource) return 'manual' as const;\n return item.externalSource.type === 'pull-request' ? ('github-pr' as const) : ('github-issue' as const);\n}\n\nfunction workItemSourceKey(item: WorkItemRow): string | null {\n return item.externalSource?.externalId ?? null;\n}\n\nasync function githubActor(\n github: GithubRulesIntegration,\n input: { installationId: number; repository: string; login: string; factoryAuthored: boolean },\n): Promise<FactoryRuleActor> {\n let trusted = false;\n try {\n const permission = await github.getRepositoryCollaboratorPermission(\n input.installationId,\n input.repository,\n input.login,\n );\n trusted = permission !== undefined && TRUSTED_PERMISSIONS.has(permission);\n } catch {\n trusted = false;\n }\n return { type: 'github', login: input.login, trusted, factoryAuthored: input.factoryAuthored };\n}\n\ninterface FactoryPullRequestProvenanceData {\n kind: 'factory-pr-provenance';\n workItemId: string;\n}\n\nfunction pullRequestProvenance(data: Record<string, unknown> | undefined): FactoryPullRequestProvenanceData | null {\n if (!data || data.kind !== 'factory-pr-provenance' || typeof data.workItemId !== 'string') return null;\n return { kind: 'factory-pr-provenance', workItemId: data.workItemId };\n}\n\nexport interface GithubRulesIntegration {\n readonly slug?: string;\n /**\n * Factory's own GitHub login, used to ignore its own writes. Optional because\n * not every integration can name itself; when absent, self-recognition falls\n * back to the configured slug and, failing that, to content Factory stamps\n * itself (see `FACTORY_TRIAGE_COMMENT_MARKER`).\n */\n readonly identity?: GithubAppIdentity;\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface GithubRulesOptions {\n github: GithubRulesIntegration;\n sourceControl: SourceControlStorageHandle;\n /** Integration-scoped storage; provenance rows are validated at read. */\n integrationStorage: IntegrationStorageHandle;\n projects: FactoryProjectsStorage;\n storage: WorkItemsStorage;\n rules: FactoryRules;\n}\n\nexport class GithubRules {\n constructor(private readonly options: GithubRulesOptions) {}\n\n /**\n * Whether a login is Factory itself. Prefers the resolved identity, which is\n * observed from Factory's own writes, and falls back to the configured slug.\n * An unset slug must not silently answer \"not Factory\" — that is what\n * disabled every self-loop guard.\n */\n #isFactoryLogin(login: string | undefined): boolean {\n const identity = this.options.github.identity;\n if (identity?.known) return identity.matches(login);\n const slug = this.options.github.slug?.trim();\n if (!slug || !login) return false;\n return login.toLowerCase() === `${slug.toLowerCase()}[bot]`;\n }\n\n async ingest(parsed: ParsedGithubWebhook): Promise<{ status: 'ignored' | 'committed' | 'replayed' | 'missing' }> {\n const event = eventName(parsed);\n const repository = object(parsed.payload.repository);\n const installationId = number(object(parsed.payload.installation)?.id);\n const repositoryId = number(repository?.id);\n const repositoryName = string(repository?.full_name);\n const login = string(object(parsed.payload.sender)?.login);\n if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: 'ignored' };\n\n const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({\n installationExternalId: String(installationId),\n repositoryExternalId: String(repositoryId),\n });\n if (projects.length === 0) return { status: 'ignored' };\n const results = [];\n for (const project of projects) {\n results.push(\n await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project),\n );\n }\n if (results.some(result => result.status === 'committed')) return { status: 'committed' };\n if (results.some(result => result.status === 'replayed')) return { status: 'replayed' };\n return results[0] ?? { status: 'ignored' };\n }\n\n async #ingestProject(\n parsed: ParsedGithubWebhook,\n event: FactoryGithubEventName,\n installationId: number,\n repositoryId: number,\n repositoryName: string,\n login: string,\n project: ExternalRepositoryProjectTarget,\n ): Promise<{ status: 'ignored' | 'committed' | 'replayed' | 'missing' }> {\n const factoryProject = await this.options.projects.get({\n orgId: project.orgId,\n id: project.factoryProjectId,\n });\n if (!factoryProject) return { status: 'missing' };\n const issue = object(parsed.payload.issue);\n const issueComment = object(parsed.payload.comment);\n const changes = object(parsed.payload.changes);\n // A comment on a PR carries the PR under `issue` (with `pull_request` set)\n // and has no `pull_request` payload of its own. Read the PR from `issue` in\n // that case so provenance and `context.pullRequest` behave as they do for\n // every other PR event, and so the number is never treated as an issue's.\n const commentOnPullRequest = object(parsed.payload.issue)?.pull_request !== undefined;\n const pullRequest = object(parsed.payload.pull_request) ?? (commentOnPullRequest ? issue : undefined);\n const issueNumber = commentOnPullRequest ? undefined : number(issue?.number);\n const pullRequestNumber = number(pullRequest?.number);\n const provenance = pullRequestNumber\n ? pullRequestProvenance(\n (\n await this.options.integrationStorage.subscriptions.listByTarget(\n provenanceTarget(repositoryId, pullRequestNumber),\n { status: 'active' },\n )\n ).find(subscription => subscription.orgId === project.orgId)?.data,\n )\n : null;\n // Re-review events target the PR's own Review card, not the Work item that\n // provenance would otherwise bind the event to. For review_requested the\n // sender is whoever clicked re-request, so a Factory-authored PR must not\n // brand a human requester as factory-authored.\n const reviewRequested = event === 'pullRequestReviewRequested';\n // Provenance proves the *pull request* came from Factory, which is not the\n // same as the sender of this event. For events where the sender is whoever\n // reacted to the PR — re-requesting review, commenting, submitting a review\n // — branding them from provenance would mark every human and every review\n // bot as Factory. Only the app login identifies Factory for those.\n const senderIsResponder =\n reviewRequested || event === 'pullRequestCommentCreated' || event === 'pullRequestReviewSubmitted';\n const reReviewEvent = reviewRequested || event === 'pullRequestUpdated';\n const requestedReviewer = string(object(parsed.payload.requested_reviewer)?.login);\n const relatedItem = await this.#relatedItem(\n project.orgId,\n project.factoryProjectId,\n repositoryId,\n repositoryName,\n issueNumber,\n pullRequestNumber,\n string(object(pullRequest?.head)?.ref),\n reReviewEvent ? null : provenance,\n senderIsResponder && !reviewRequested,\n );\n const actor = await githubActor(this.options.github, {\n installationId,\n repository: repositoryName,\n login,\n factoryAuthored: (!senderIsResponder && provenance !== null) || this.#isFactoryLogin(login),\n });\n // A marked handoff comment is ignored only when Factory authored it: a\n // human may quote the marker to add an investigation lead, and that must\n // still retrigger triage. Recognising the author is therefore the whole\n // guard — when identity cannot be resolved this fails open and Factory's\n // own handoff cancels the run that wrote it.\n if (\n actor.type === 'github' &&\n actor.factoryAuthored &&\n (event === 'issueCommentCreated' || event === 'issueCommentEdited') &&\n string(issueComment?.body)?.includes(FACTORY_TRIAGE_COMMENT_MARKER)\n ) {\n return { status: 'ignored' };\n }\n // One delivery can concern two cards — a merged pull request settles both\n // its own Review card and the Work item that authored it — and every\n // decision a rule returns is committed against a single item, at that\n // item's revision. So the evaluation, not the decision, is what fans out:\n // the rule runs once per bound item, each with its own ingress identity.\n const evaluate = async (\n item: WorkItemRow | undefined,\n ingressIdentity: string,\n ): Promise<{ status: 'ignored' | 'committed' | 'replayed' | 'missing' }> => {\n const context: FactoryGithubRuleContext = {\n tenant: { orgId: project.orgId, projectId: project.factoryProjectId },\n actor,\n ingress: { type: 'github', id: ingressIdentity },\n cause: `github.${event}`,\n causalChain: [],\n ruleSetVersion: this.options.rules.version,\n ...(item\n ? {\n item: {\n id: item.id,\n source: workItemSource(item),\n sourceKey: workItemSourceKey(item),\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n url: item.externalSource?.url ?? null,\n stages: item.stages,\n metadata: item.metadata,\n },\n board: item.externalSource?.type === 'pull-request' ? ('review' as const) : ('work' as const),\n itemRevision: item.revision,\n }\n : {}),\n event,\n deliveryId: parsed.deliveryId,\n factory: { createdAt: factoryProject.createdAt.toISOString() },\n repository: { id: repositoryId, fullName: repositoryName },\n ...(issueNumber && string(issue?.title) && string(issue?.html_url)\n ? {\n issue: {\n number: issueNumber,\n title: string(issue?.title)!,\n url: string(issue?.html_url)!,\n ...(string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}),\n ...(string(issue?.updated_at) ? { updatedAt: string(issue?.updated_at) } : {}),\n assignees: actorLogins(issue?.assignees),\n labels: labelNames(issue?.labels),\n ...(string(issue?.state) === 'closed' || string(issue?.state) === 'open'\n ? { state: string(issue?.state) as 'open' | 'closed' }\n : {}),\n ...(string(issue?.state_reason) ? { stateReason: string(issue?.state_reason) } : {}),\n },\n }\n : {}),\n ...(event === 'issueEdited'\n ? { issueChange: { title: Boolean(object(changes?.title)), body: Boolean(object(changes?.body)) } }\n : {}),\n ...(number(issueComment?.id)\n ? {\n issueComment: {\n id: number(issueComment?.id)!,\n ...(string(issueComment?.body) ? { body: string(issueComment?.body) } : {}),\n ...(string(issueComment?.html_url) ? { url: string(issueComment?.html_url) } : {}),\n ...(string(object(issueComment?.user)?.login)\n ? { author: string(object(issueComment?.user)?.login) }\n : {}),\n ...(string(object(issueComment?.user)?.type)\n ? { authorType: string(object(issueComment?.user)?.type) }\n : {}),\n ...(string(issueComment?.created_at) ? { createdAt: string(issueComment?.created_at) } : {}),\n ...(string(issueComment?.updated_at) ? { updatedAt: string(issueComment?.updated_at) } : {}),\n },\n }\n : {}),\n ...(pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url)\n ? {\n pullRequest: {\n number: pullRequestNumber,\n title: string(pullRequest?.title)!,\n url: string(pullRequest?.html_url)!,\n ...(string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {}),\n state: string(pullRequest?.state) === 'closed' ? ('closed' as const) : ('open' as const),\n draft: boolean(pullRequest?.draft) ?? false,\n merged: boolean(pullRequest?.merged) ?? false,\n assignees: actorLogins(pullRequest?.assignees),\n requestedReviewers: actorLogins(pullRequest?.requested_reviewers),\n labels: labelNames(pullRequest?.labels),\n headBranch: string(object(pullRequest?.head)?.ref) ?? '',\n baseBranch: string(object(pullRequest?.base)?.ref) ?? '',\n },\n }\n : {}),\n ...(reviewRequested && requestedReviewer\n ? {\n reviewRequest: {\n reviewer: requestedReviewer,\n factoryReviewer: this.#isFactoryLogin(requestedReviewer),\n },\n }\n : {}),\n ...(object(parsed.payload.review)\n ? {\n review: {\n id: number(object(parsed.payload.review)?.id) ?? 0,\n state: string(object(parsed.payload.review)?.state) ?? 'unknown',\n url: string(object(parsed.payload.review)?.html_url) ?? '',\n },\n }\n : {}),\n };\n\n const rule = resolveFactoryGithubRule(this.options.rules, 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 GitHub rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: project.orgId,\n factoryProjectId: project.factoryProjectId,\n workItemId: item?.id ?? null,\n ingress: { identity: ingressIdentity, triggerType: `github.${event}` },\n ruleSetVersion: this.options.rules.version,\n expectedRevision: item?.revision ?? null,\n actor: { ...actor },\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n return { status: committed.status };\n };\n\n const deliveryIdentity = `${installationId}:${parsed.deliveryId}`;\n const primary = await evaluate(relatedItem, deliveryIdentity);\n // A merged pull request is the one event both linked cards need: the\n // Review card has to close, and the Work item that wrote the code has to\n // assess whether it is finished. Resolution binds the delivery to whichever\n // card it matched first, so evaluate the other one too — under an identity\n // suffixed with its id, because ingress identities are the replay key and\n // reusing the delivery's own would drop this evaluation as a duplicate.\n const linked =\n event === 'pullRequestMerged' && relatedItem && pullRequestNumber\n ? await this.#linkedClosureItem(\n project.orgId,\n project.factoryProjectId,\n repositoryId,\n repositoryName,\n pullRequestNumber,\n relatedItem,\n )\n : undefined;\n if (!linked) return primary;\n const secondary = await evaluate(linked, `${deliveryIdentity}:${linked.id}`);\n for (const status of ['committed', 'replayed'] as const) {\n if (primary.status === status || secondary.status === status) return { status };\n }\n return primary;\n }\n\n /**\n * The other card a closed pull request concerns, joined through the Review\n * card's `parentWorkItemId` — the link `upsertLinkedWorkItem` records when the\n * pull request is opened, and therefore an exact join that needs no branch\n * heuristics. Returns nothing when the pull request has only one card, which\n * is every pull request Factory did not open from a work item's session.\n */\n async #linkedClosureItem(\n orgId: string,\n projectId: string,\n repositoryId: number,\n repositoryFullName: string,\n pullRequestNumber: number,\n resolved: WorkItemRow,\n ): Promise<WorkItemRow | undefined> {\n const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });\n const linked =\n resolved.externalSource?.type === 'pull-request'\n ? // Bound to the pull request's own Review card: follow the recorded\n // link back to the work item that authored it.\n items.find(item => item.id === resolved.parentWorkItemId)\n : // Bound to the work item (provenance): find the Review card this pull\n // request opened, and only when it names this item as its parent — a\n // card for the same number in another repository is not this one's.\n items.find(\n item =>\n item.parentWorkItemId === resolved.id &&\n (item.externalSource?.externalId === canonicalSourceKey('pull-request', pullRequestNumber) ||\n item.externalSource?.externalId === legacySourceKey(repositoryId, 'pull-request', pullRequestNumber)) &&\n cardBelongsToRepository(item, repositoryId, repositoryFullName),\n );\n return linked?.id === resolved.id ? undefined : linked;\n }\n\n async #relatedItem(\n orgId: string,\n projectId: string,\n repositoryId: number,\n repositoryFullName: string,\n issueNumber: number | undefined,\n pullRequestNumber: number | undefined,\n pullRequestHeadBranch: string | undefined,\n provenance: FactoryPullRequestProvenanceData | null,\n preferAuthoringItem = false,\n ): Promise<WorkItemRow | undefined> {\n const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });\n const resolved = this.#resolveItem(\n items,\n repositoryId,\n repositoryFullName,\n issueNumber,\n pullRequestNumber,\n pullRequestHeadBranch,\n provenance,\n );\n // Feedback on a pull request has to reach the item that *wrote* the code.\n // Provenance normally lands it there directly, but when provenance is\n // missing the PR-number lookup wins and returns the PR's own Review card\n // instead — a board the feedback rules deliberately refuse to act on, so\n // the wake is silently dropped. The linked card records its author in\n // `parentWorkItemId`, so follow that link back rather than relaxing the\n // guard, which would let a Review card react to its own posted review.\n if (preferAuthoringItem && resolved?.externalSource?.type === 'pull-request' && resolved.parentWorkItemId) {\n return items.find(item => item.id === resolved.parentWorkItemId) ?? resolved;\n }\n return resolved;\n }\n\n #resolveItem(\n items: WorkItemRow[],\n repositoryId: number,\n repositoryFullName: string,\n issueNumber: number | undefined,\n pullRequestNumber: number | undefined,\n pullRequestHeadBranch: string | undefined,\n provenance: FactoryPullRequestProvenanceData | null,\n ): WorkItemRow | undefined {\n if (provenance) return items.find(item => item.id === provenance.workItemId);\n if (issueNumber) {\n return (\n items.find(\n item =>\n item.externalSource?.externalId === canonicalSourceKey('issue', issueNumber) &&\n cardBelongsToRepository(item, repositoryId, repositoryFullName),\n ) ?? items.find(item => item.externalSource?.externalId === legacySourceKey(repositoryId, 'issue', issueNumber))\n );\n }\n if (pullRequestNumber) {\n return (\n items.find(\n item =>\n item.externalSource?.externalId === canonicalSourceKey('pull-request', pullRequestNumber) &&\n cardBelongsToRepository(item, repositoryId, repositoryFullName),\n ) ??\n items.find(\n item => item.externalSource?.externalId === legacySourceKey(repositoryId, 'pull-request', pullRequestNumber),\n ) ??\n // Provenance fallback: a PR pushed from a work item's session branch\n // belongs to that item even when no gh-pr-create provenance was\n // recorded (session predating state seeding, or the PR was opened\n // outside the tracked tool call). Session branches are per-item\n // (`factory/issue-N`), so a head-branch match is unambiguous.\n (pullRequestHeadBranch\n ? items.find(\n item =>\n item.externalSource?.type !== 'pull-request' &&\n Object.values(item.sessions).some(session => session.branch === pullRequestHeadBranch),\n )\n : undefined)\n );\n }\n return undefined;\n }\n}\n\nexport interface ReconcilePullRequestState {\n title: string;\n url: string;\n state: 'open' | 'closed';\n draft: boolean;\n merged: boolean;\n assignees?: string[];\n requestedReviewers?: string[];\n labels?: string[];\n headBranch: string;\n baseBranch: string;\n author?: string;\n createdAt?: string;\n mergedBy?: string;\n}\n\nexport type GithubPullRequestFetcher = (input: {\n installationId: number;\n repository: string;\n number: number;\n}) => Promise<ReconcilePullRequestState | undefined>;\n\nexport interface ReconcileIssueState {\n title: string;\n url: string;\n state: 'open' | 'closed';\n /** GitHub close reason: `completed`, `not_planned`, or `duplicate`. */\n stateReason?: string;\n assignees?: string[];\n labels?: string[];\n author?: string;\n createdAt?: string;\n updatedAt?: string;\n}\n\nexport type GithubIssueFetcher = (input: {\n installationId: number;\n repository: string;\n number: number;\n}) => Promise<ReconcileIssueState | undefined>;\n\nexport interface ReconcileRepository {\n id: number;\n fullName: string;\n installationId: number;\n}\n\nexport interface ReconcileSweepSummary {\n /** Factory-configured repositories included in the sweep. */\n repositories: number;\n /** PRs whose live state was fetched from GitHub. */\n checked: number;\n /** Missed merges replayed through the rules ingress. */\n merged: number;\n /** Missed closes-without-merge replayed through the rules ingress. */\n closed: number;\n /** PRs/issues (or whole repositories) skipped because of an error. */\n failed: number;\n /** Error samples with context, capped at {@link RECONCILE_ERROR_SAMPLE_LIMIT}. */\n errors: Array<{ repository: string; pullRequestNumber?: number; issueNumber?: number; error: string }>;\n}\n\nexport type GithubPullRequestReconciler = (repositories: ReconcileRepository[]) => Promise<ReconcileSweepSummary>;\n\nexport const RECONCILE_ERROR_SAMPLE_LIMIT = 5;\n\nexport function sameStrings(left: unknown, right: string[] | undefined): boolean {\n if (right === undefined) return true;\n if (!Array.isArray(left)) return false;\n const leftValues = new Set(left.flatMap(value => (typeof value === 'string' ? [value] : [])));\n const rightValues = new Set(right);\n return leftValues.size === rightValues.size && [...leftValues].every(value => rightValues.has(value));\n}\n\n/**\n * Extracts the PR number a work item tracks, but only when the item belongs\n * to the given repository. Card URLs pin the repository unambiguously; the\n * legacy source key embeds the repository id. Canonical keys (`github-pr:N`)\n * carry no repository, so they are only trusted when the item has no URL —\n * a project mapped to multiple repositories must not reconcile one repo's\n * card against another repo's PR number.\n */\nfunction reconcilablePullRequestNumber(item: WorkItemRow, repository: ReconcileRepository): number | undefined {\n if (item.externalSource?.type !== 'pull-request') return undefined;\n const url = item.externalSource.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/pull\\/(\\d+)(?:[/?#]|$)/.exec(url);\n if (!match) return undefined;\n return match[1] === repository.fullName ? Number(match[2]) : undefined;\n }\n const externalId = item.externalSource.externalId;\n const legacy = /^github:(\\d+):pull-request:(\\d+)$/.exec(externalId);\n if (legacy) return Number(legacy[1]) === repository.id ? Number(legacy[2]) : undefined;\n const canonical = /^github-pr:(\\d+)$/.exec(externalId);\n return canonical ? Number(canonical[1]) : undefined;\n}\n\n/**\n * Extracts the issue number a work item tracks, but only when the item\n * belongs to the given repository. Stricter than\n * {@link reconcilablePullRequestNumber}: a canonical key with no URL is only\n * trusted when the intake-stamped `githubRepositoryId` confirms the\n * repository, because the sweep initiates closes on its own.\n */\nexport function reconcilableIssueNumber(item: WorkItemRow, repository: ReconcileRepository): number | undefined {\n if (item.externalSource?.type !== 'issue') return undefined;\n const url = item.externalSource.url;\n if (url) {\n const match = /^https?:\\/\\/[^/]+\\/(.+)\\/issues\\/(\\d+)(?:[/?#]|$)/.exec(url);\n if (!match) return undefined;\n // A renamed repository leaves the old owner/name in the card URL, so a\n // URL mismatch still defers to the stable intake-stamped repository id.\n const belongs = match[1] === repository.fullName || item.metadata?.githubRepositoryId === repository.id;\n return belongs ? Number(match[2]) : undefined;\n }\n const externalId = item.externalSource.externalId;\n const legacy = /^github:(\\d+):issue:(\\d+)$/.exec(externalId);\n if (legacy) return Number(legacy[1]) === repository.id ? Number(legacy[2]) : undefined;\n const canonical = /^github-issue:(\\d+)$/.exec(externalId);\n if (!canonical) return undefined;\n // Canonical keys carry no repository; only the intake-stamped repository id\n // can attribute a URL-less card, and guessing would let a multi-repo\n // project close repo B's card because repo A's same-numbered issue closed.\n return item.metadata?.githubRepositoryId === repository.id ? Number(canonical[1]) : undefined;\n}\n\nexport function reconciledIssueClosedEvent(\n repository: ReconcileRepository,\n issueNumber: number,\n state: ReconcileIssueState,\n): ParsedGithubWebhook {\n return {\n event: 'issues',\n // Stable per (repository, issue): the ingress dedupe makes repeat\n // reconcile cycles replay instead of re-committing decisions.\n deliveryId: `reconcile:${repository.id}:issue:${issueNumber}:closed`,\n payload: {\n action: 'closed',\n installation: { id: repository.installationId },\n repository: { id: repository.id, full_name: repository.fullName },\n sender: { login: state.author ?? 'github' },\n issue: {\n number: issueNumber,\n title: state.title,\n html_url: state.url,\n state: 'closed',\n ...(state.stateReason ? { state_reason: state.stateReason } : {}),\n ...(state.createdAt ? { created_at: state.createdAt } : {}),\n ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),\n assignees: (state.assignees ?? []).map(login => ({ login })),\n },\n },\n };\n}\n\nexport function reconciledClosedEvent(\n repository: ReconcileRepository,\n pullRequestNumber: number,\n state: ReconcilePullRequestState,\n): ParsedGithubWebhook {\n return {\n event: 'pull_request',\n // Stable per (repository, PR, outcome): the ingress dedupe makes repeat\n // reconcile cycles replay instead of re-committing decisions.\n deliveryId: `reconcile:${repository.id}:pull-request:${pullRequestNumber}:${state.merged ? 'merged' : 'closed'}`,\n payload: {\n action: 'closed',\n installation: { id: repository.installationId },\n repository: { id: repository.id, full_name: repository.fullName },\n sender: { login: state.mergedBy ?? 'github' },\n pull_request: {\n number: pullRequestNumber,\n title: state.title,\n html_url: state.url,\n ...(state.createdAt ? { created_at: state.createdAt } : {}),\n state: 'closed',\n draft: state.draft,\n merged: state.merged,\n assignees: (state.assignees ?? []).map(login => ({ login })),\n requested_reviewers: (state.requestedReviewers ?? []).map(login => ({ login })),\n labels: (state.labels ?? []).map(name => ({ name })),\n head: { ref: state.headBranch },\n base: { ref: state.baseBranch },\n },\n },\n };\n}\n\nfunction reconciledPullRequestMetadata(\n state: ReconcilePullRequestState,\n reconciliation: 'clear' | 'settled',\n): Record<string, unknown> {\n return {\n state: state.state,\n draft: state.draft,\n merged: state.merged,\n ...(state.author ? { author: state.author } : {}),\n ...(state.assignees ? { assignees: state.assignees } : {}),\n ...(state.requestedReviewers ? { requestedReviewers: state.requestedReviewers } : {}),\n ...(state.labels ? { labels: state.labels } : {}),\n [FACTORY_PULL_REQUEST_RECONCILIATION_KEY]:\n reconciliation === 'settled' ? (state.merged ? 'merged' : 'closed') : null,\n };\n}\n\nfunction reconciledPullRequestOutcome(metadata: Record<string, unknown>): 'merged' | 'closed' | undefined {\n if (metadata.state !== 'closed' || typeof metadata.merged !== 'boolean') return undefined;\n return metadata.merged ? 'merged' : 'closed';\n}\n\n/**\n * The webhook handler retires subscriptions itself; the sweep replays only the\n * rules ingress, so without this the thread's PR chip and the workspace row\n * stay `open` forever on a deployment GitHub cannot reach.\n */\nasync function retireReconciledSubscriptions(\n storage: IntegrationStorageHandle,\n repository: ReconcileRepository,\n pullRequestNumber: number,\n merged: boolean,\n): Promise<void> {\n const target = changeRequestTargetKey({\n installationExternalId: String(repository.installationId),\n repositoryExternalId: String(repository.id),\n changeRequestId: String(pullRequestNumber),\n });\n const rows = await storage.subscriptions.listByTarget(target);\n await Promise.all(\n rows\n .filter(row => row.status === 'open')\n .map(row => storage.subscriptions.updateStatus(row.id, merged ? 'merged' : 'closed')),\n );\n}\n\n/**\n * State-based safety net for merge signals: webhooks and event-log tailing\n * can miss a merge (cursor gaps, downtime, terminally failed decisions), so\n * this sweep compares still-open PR cards against actual GitHub state and\n * replays the merge through the normal rules ingress when they disagree.\n */\nexport function createGithubPullRequestReconciler(\n options: GithubRulesOptions,\n fetchPullRequest: GithubPullRequestFetcher,\n): GithubPullRequestReconciler {\n const rules = new GithubRules(options);\n return async repositories => {\n const summary: ReconcileSweepSummary = {\n repositories: 0,\n checked: 0,\n merged: 0,\n closed: 0,\n failed: 0,\n errors: [],\n };\n const recordFailure = (repository: ReconcileRepository, error: unknown, pullRequestNumber?: number) => {\n summary.failed += 1;\n if (summary.errors.length < RECONCILE_ERROR_SAMPLE_LIMIT) {\n summary.errors.push({\n repository: repository.fullName,\n ...(pullRequestNumber === undefined ? {} : { pullRequestNumber }),\n error: error instanceof Error ? error.message : String(error),\n });\n }\n };\n // An installation can expose hundreds of repositories; only the ones\n // actually linked to a factory project can have cards to reconcile, so\n // scope the sweep to those up front instead of probing each repository.\n const configured = new Set(\n (await options.sourceControl.projectRepositories.listConfiguredExternalKeys()).map(\n key => `${key.installationExternalId}\\u0000${key.repositoryExternalId}`,\n ),\n );\n const scoped = repositories.filter(repository =>\n configured.has(`${repository.installationId}\\u0000${repository.id}`),\n );\n summary.repositories = scoped.length;\n for (const repository of scoped) {\n // One broken repository (or a failing token exchange for its\n // installation) must not abort the sweep for the others.\n let cardsByNumber: Map<number, WorkItemRow[]>;\n try {\n const projects = await options.sourceControl.projectRepositories.listByExternalRepository({\n installationExternalId: String(repository.installationId),\n repositoryExternalId: String(repository.id),\n });\n if (projects.length === 0) continue;\n cardsByNumber = new Map<number, WorkItemRow[]>();\n for (const project of projects) {\n const items = await options.storage.list({\n orgId: project.orgId,\n factoryProjectId: project.factoryProjectId,\n });\n for (const item of items) {\n const stage = item.stages[0];\n const pullRequestNumber = reconcilablePullRequestNumber(item, repository);\n if (!pullRequestNumber) continue;\n const metadata = item.metadata ?? {};\n const reconciliation = metadata[FACTORY_PULL_REQUEST_RECONCILIATION_KEY];\n const reconciledOutcome = reconciledPullRequestOutcome(metadata);\n if (\n (stage === 'done' || stage === 'canceled') &&\n reconciledOutcome !== undefined &&\n reconciliation === reconciledOutcome\n ) {\n continue;\n }\n const cards = cardsByNumber.get(pullRequestNumber) ?? [];\n cards.push(item);\n cardsByNumber.set(pullRequestNumber, cards);\n }\n }\n } catch (error) {\n recordFailure(repository, error);\n continue;\n }\n for (const [pullRequestNumber, cards] of cardsByNumber) {\n try {\n const state = await fetchPullRequest({\n installationId: repository.installationId,\n repository: repository.fullName,\n number: pullRequestNumber,\n });\n summary.checked += 1;\n if (!state) continue;\n for (const card of cards) {\n if (state.state === 'closed') continue;\n const metadata = card.metadata ?? {};\n const statusChanged =\n metadata.state !== state.state || metadata.draft !== state.draft || metadata.merged !== state.merged;\n const authorChanged = state.author !== undefined && metadata.author !== state.author;\n const assigneesChanged = !sameStrings(metadata.assignees, state.assignees);\n const reviewersChanged = !sameStrings(metadata.requestedReviewers, state.requestedReviewers);\n const labelsChanged = !sameStrings(metadata.labels, state.labels);\n const metadataChanged =\n statusChanged || authorChanged || assigneesChanged || reviewersChanged || labelsChanged;\n const reconciliation = metadata[FACTORY_PULL_REQUEST_RECONCILIATION_KEY];\n if (!metadataChanged && reconciliation !== 'merged' && reconciliation !== 'closed') continue;\n try {\n await options.storage.update({\n orgId: card.orgId,\n id: card.id,\n userId: 'factory-rule-dispatcher',\n patch: { metadata: reconciledPullRequestMetadata(state, 'clear') },\n });\n } catch (error) {\n recordFailure(repository, error, pullRequestNumber);\n }\n }\n if (state.state !== 'closed') continue;\n const cleanupFailures = new Set<string>();\n for (const card of cards) {\n if (!isTerminalFactoryRuleStage(card.stages)) continue;\n try {\n await options.storage.supersedeDecisionsForWorkItem({\n orgId: card.orgId,\n factoryProjectId: card.factoryProjectId,\n workItemId: card.id,\n supersededAt: new Date(),\n });\n } catch (error) {\n recordFailure(repository, error, pullRequestNumber);\n cleanupFailures.add(card.id);\n }\n }\n await rules.ingest(reconciledClosedEvent(repository, pullRequestNumber, state));\n await retireReconciledSubscriptions(options.integrationStorage, repository, pullRequestNumber, state.merged);\n for (const card of cards) {\n if (cleanupFailures.has(card.id)) continue;\n try {\n await options.storage.update({\n orgId: card.orgId,\n id: card.id,\n userId: 'factory-rule-dispatcher',\n patch: { metadata: reconciledPullRequestMetadata(state, 'settled') },\n });\n } catch (error) {\n recordFailure(repository, error, pullRequestNumber);\n }\n }\n if (state.merged) summary.merged += 1;\n else summary.closed += 1;\n } catch (error) {\n recordFailure(repository, error, pullRequestNumber);\n }\n }\n }\n return summary;\n };\n}\n\nexport function githubRulesOptions(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n): GithubRulesOptions | undefined {\n if (!context.rules) return undefined;\n return {\n github,\n sourceControl: context.storage.sourceControl,\n integrationStorage: context.storage.generic,\n projects: context.storage.projects,\n storage: context.rules.workItems,\n rules: context.rules.config,\n };\n}\n\nexport function attachGithubRules(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n): ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined {\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n const rules = new GithubRules(options);\n return event => rules.ingest(event);\n}\n\nexport function attachGithubReconciler(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n fetchPullRequest: GithubPullRequestFetcher,\n): GithubPullRequestReconciler | undefined {\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n return createGithubPullRequestReconciler(options, fetchPullRequest);\n}\n"],"mappings":"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"}
@@ -2,13 +2,22 @@ import type { RequestContext } from '@mastra/core/request-context';
2
2
  import type { GithubIntegration } from './integration.js';
3
3
  export declare function subscribeCurrentSessionToPullRequest(requestContext: RequestContext, pullRequest: number | string, source: 'auto-gh-pr-create' | 'explicit-tool', github: GithubIntegration): Promise<number | undefined>;
4
4
  export declare function unsubscribeCurrentSessionFromPullRequest(requestContext: RequestContext, pullRequest: number | string, github: GithubIntegration): Promise<number>;
5
+ export declare function upsertFactoryTriageComment(requestContext: RequestContext, input: {
6
+ issueNumber: number;
7
+ body: string;
8
+ }, github: GithubIntegration): Promise<import("./integration.js").GithubTriageCommentUpsertResult>;
5
9
  export declare function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void>;
6
10
  export declare function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration): {
7
11
  github_refresh_token?: undefined;
12
+ github_upsert_factory_triage_comment?: undefined;
8
13
  github_subscribe_pr?: undefined;
9
14
  github_unsubscribe_pr?: undefined;
10
15
  } | {
11
16
  github_refresh_token: import("@mastra/core/tools").Tool<Record<string, never>, unknown, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "github_refresh_token", unknown>;
17
+ github_upsert_factory_triage_comment: import("@mastra/core/tools").Tool<{
18
+ issueNumber: number;
19
+ body: string;
20
+ }, unknown, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "github_upsert_factory_triage_comment", unknown>;
12
21
  github_subscribe_pr: import("@mastra/core/tools").Tool<{
13
22
  pullRequest: string | number;
14
23
  }, unknown, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "github_subscribe_pr", unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"session-subscriptions.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/session-subscriptions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAUnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAgH1D,wBAAsB,oCAAoC,CACxD,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,MAAM,EAAE,mBAAmB,GAAG,eAAe,EAC7C,MAAM,EAAE,iBAAiB,+BAY1B;AAED,wBAAsB,wCAAwC,CAC5D,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,MAAM,EAAE,iBAAiB,mBAM1B;AAED,wBAAsB,kBAAkB,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBjH;AAED,wBAAgB,6BAA6B,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB;;;;;;;;;;;;EAmCtG;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgB1D;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,sBAcA"}
1
+ {"version":3,"file":"session-subscriptions.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/session-subscriptions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAUnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAyI1D,wBAAsB,oCAAoC,CACxD,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,MAAM,EAAE,mBAAmB,GAAG,eAAe,EAC7C,MAAM,EAAE,iBAAiB,+BAY1B;AAED,wBAAsB,wCAAwC,CAC5D,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,MAAM,EAAE,iBAAiB,mBAM1B;AAED,wBAAsB,0BAA0B,CAC9C,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC5C,MAAM,EAAE,iBAAiB,uEAa1B;AAED,wBAAsB,kBAAkB,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBjH;AAED,wBAAgB,6BAA6B,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB;;;;;;;;;;;;;;;;;EA0CtG;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgB1D;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,sBAcA"}
@@ -19,6 +19,27 @@ function sessionOrgId(requestContext) {
19
19
  return getFactoryAuthOrgId(getFactoryAuthUserFromContext(requestContext));
20
20
  }
21
21
  const pullRequestInputSchema = z.object({ pullRequest: z.union([z.number().int().positive(), z.string().min(1)]) });
22
+ const triageCommentInputSchema = z.object({
23
+ issueNumber: z.number().int().positive(),
24
+ body: z.string().startsWith("<!-- mastra-factory-triage -->")
25
+ });
26
+ const triageCommentLocks = /* @__PURE__ */ new Map();
27
+ async function serializeTriageComment(key, operation) {
28
+ const previous = triageCommentLocks.get(key) ?? Promise.resolve();
29
+ let release;
30
+ const current = new Promise((resolve) => {
31
+ release = resolve;
32
+ });
33
+ const queued = previous.then(() => current);
34
+ triageCommentLocks.set(key, queued);
35
+ await previous;
36
+ try {
37
+ return await operation();
38
+ } finally {
39
+ release();
40
+ if (triageCommentLocks.get(key) === queued) triageCommentLocks.delete(key);
41
+ }
42
+ }
22
43
  function parsePullRequest(value, expectedRepo) {
23
44
  if (typeof value === "number") return value;
24
45
  if (/^\d+$/.test(value)) return Number(value);
@@ -116,6 +137,17 @@ async function unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequ
116
137
  await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);
117
138
  return number;
118
139
  }
140
+ async function upsertFactoryTriageComment(requestContext, input, github) {
141
+ const target = await resolveSessionTarget(requestContext, github);
142
+ const installationId = Number(target.installation.externalId);
143
+ if (!Number.isSafeInteger(installationId) || installationId <= 0) throw new Error("GitHub installation is invalid.");
144
+ return serializeTriageComment(`${installationId}:${target.repository.externalId}:${input.issueNumber}`, () => github.upsertFactoryTriageComment({
145
+ installationId,
146
+ repository: target.repository.slug,
147
+ issueNumber: input.issueNumber,
148
+ body: input.body
149
+ }));
150
+ }
119
151
  async function refreshGithubToken(requestContext, github) {
120
152
  const target = await resolveSessionTarget(requestContext, github);
121
153
  const pat = await getGithubPat(() => github.integrationStorage, target.orgId, getRegisteredGithubPatKind(requestContext));
@@ -142,6 +174,12 @@ function createGithubSubscriptionTools(requestContext, github) {
142
174
  return { refreshed: true };
143
175
  }
144
176
  }),
177
+ github_upsert_factory_triage_comment: createTool({
178
+ id: "github_upsert_factory_triage_comment",
179
+ description: "Create or update this Factory App’s canonical triage handoff comment on an issue in the active repository. Use this for every marked pending or final Factory triage handoff; never use gh to create or edit that handoff.",
180
+ inputSchema: triageCommentInputSchema,
181
+ execute: async (input) => upsertFactoryTriageComment(requestContext, input, github)
182
+ }),
145
183
  github_subscribe_pr: createTool({
146
184
  id: "github_subscribe_pr",
147
185
  description: "Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.",
@@ -191,6 +229,6 @@ function parseCreatedPullRequest(context) {
191
229
  return urls.length === 1 ? urls[0] : void 0;
192
230
  }
193
231
  //#endregion
194
- export { createGithubSubscriptionTools, parseCreatedPullRequest, refreshGithubToken, stripHeredocBodies, subscribeCurrentSessionToPullRequest, unsubscribeCurrentSessionFromPullRequest };
232
+ export { createGithubSubscriptionTools, parseCreatedPullRequest, refreshGithubToken, stripHeredocBodies, subscribeCurrentSessionToPullRequest, unsubscribeCurrentSessionFromPullRequest, upsertFactoryTriageComment };
195
233
 
196
234
  //# sourceMappingURL=session-subscriptions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-subscriptions.js","names":[],"sources":["../../../src/integrations/github/session-subscriptions.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserId } from '../../auth.js';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { getGithubPat } from './pat.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { getRegisteredGithubPatKind, injectGithubToken } from './token-refresh.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * The host-authenticated user placed on the request context under the `user`\n * key, read through the host's own normalizer. A local mirror of that shape\n * used to live here; it silently missed the provider shapes the normalizer\n * knows about, which turned every subscription tool into a no-op for those\n * users instead of an error anybody could see.\n */\nfunction sessionUserId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthUserId(getFactoryAuthUserFromContext(requestContext));\n}\n\nfunction sessionOrgId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthOrgId(getFactoryAuthUserFromContext(requestContext));\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n return Boolean(\n context?.threadId &&\n context.getState().projectRepositoryId &&\n sessionOrgId(requestContext) &&\n sessionUserId(requestContext),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const orgId = sessionOrgId(requestContext);\n const userId = sessionUserId(requestContext);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport async function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void> {\n const target = await resolveSessionTarget(requestContext, github);\n // `GH_TOKEN` feeds the `gh` CLI, so a configured org PAT wins over a minted\n // installation token (which 403s on integration-restricted endpoints). The\n // workspace records which PAT kind the sandbox was provisioned with, so a\n // review-board sandbox keeps its reviewer token on refresh.\n const pat = await getGithubPat(\n () => github.integrationStorage,\n target.orgId,\n getRegisteredGithubPatKind(requestContext),\n );\n if (pat) {\n injectGithubToken(requestContext, pat);\n return;\n }\n const access = await github.versionControl.getRepositoryAccess({\n orgId: target.orgId,\n repositoryId: target.repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session.');\n injectGithubToken(requestContext, token);\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n if (!isGithubProjectSession(requestContext)) return {};\n\n return {\n github_refresh_token: createTool({\n id: 'github_refresh_token',\n description:\n 'Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.',\n inputSchema: z.object({}),\n execute: async () => {\n await refreshGithubToken(requestContext, github);\n return { refreshed: true };\n },\n }),\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nexport function stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.toolName !== 'execute_command' || context.error) return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (\n typeof command !== 'string' ||\n !/(?:^|\\n|;|&&|\\|\\|)\\s*gh\\s+pr\\s+create(?:\\s|$)/.test(stripHeredocBodies(command))\n ) {\n return undefined;\n }\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-subscriptions.js","names":[],"sources":["../../../src/integrations/github/session-subscriptions.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserId } from '../../auth.js';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { getGithubPat } from './pat.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { getRegisteredGithubPatKind, injectGithubToken } from './token-refresh.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * The host-authenticated user placed on the request context under the `user`\n * key, read through the host's own normalizer. A local mirror of that shape\n * used to live here; it silently missed the provider shapes the normalizer\n * knows about, which turned every subscription tool into a no-op for those\n * users instead of an error anybody could see.\n */\nfunction sessionUserId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthUserId(getFactoryAuthUserFromContext(requestContext));\n}\n\nfunction sessionOrgId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthOrgId(getFactoryAuthUserFromContext(requestContext));\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\nconst TRIAGE_COMMENT_MARKER = '<!-- mastra-factory-triage -->';\nconst triageCommentInputSchema = z.object({\n issueNumber: z.number().int().positive(),\n body: z.string().startsWith(TRIAGE_COMMENT_MARKER),\n});\n\nconst triageCommentLocks = new Map<string, Promise<void>>();\n\nasync function serializeTriageComment<T>(key: string, operation: () => Promise<T>): Promise<T> {\n const previous = triageCommentLocks.get(key) ?? Promise.resolve();\n let release: (() => void) | undefined;\n const current = new Promise<void>(resolve => {\n release = resolve;\n });\n const queued = previous.then(() => current);\n triageCommentLocks.set(key, queued);\n await previous;\n try {\n return await operation();\n } finally {\n release!();\n if (triageCommentLocks.get(key) === queued) triageCommentLocks.delete(key);\n }\n}\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n return Boolean(\n context?.threadId &&\n context.getState().projectRepositoryId &&\n sessionOrgId(requestContext) &&\n sessionUserId(requestContext),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const orgId = sessionOrgId(requestContext);\n const userId = sessionUserId(requestContext);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport async function upsertFactoryTriageComment(\n requestContext: RequestContext,\n input: { issueNumber: number; body: string },\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const installationId = Number(target.installation.externalId);\n if (!Number.isSafeInteger(installationId) || installationId <= 0) throw new Error('GitHub installation is invalid.');\n return serializeTriageComment(`${installationId}:${target.repository.externalId}:${input.issueNumber}`, () =>\n github.upsertFactoryTriageComment({\n installationId,\n repository: target.repository.slug,\n issueNumber: input.issueNumber,\n body: input.body,\n }),\n );\n}\n\nexport async function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void> {\n const target = await resolveSessionTarget(requestContext, github);\n // `GH_TOKEN` feeds the `gh` CLI, so a configured org PAT wins over a minted\n // installation token (which 403s on integration-restricted endpoints). The\n // workspace records which PAT kind the sandbox was provisioned with, so a\n // review-board sandbox keeps its reviewer token on refresh.\n const pat = await getGithubPat(\n () => github.integrationStorage,\n target.orgId,\n getRegisteredGithubPatKind(requestContext),\n );\n if (pat) {\n injectGithubToken(requestContext, pat);\n return;\n }\n const access = await github.versionControl.getRepositoryAccess({\n orgId: target.orgId,\n repositoryId: target.repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session.');\n injectGithubToken(requestContext, token);\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n if (!isGithubProjectSession(requestContext)) return {};\n\n return {\n github_refresh_token: createTool({\n id: 'github_refresh_token',\n description:\n 'Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.',\n inputSchema: z.object({}),\n execute: async () => {\n await refreshGithubToken(requestContext, github);\n return { refreshed: true };\n },\n }),\n github_upsert_factory_triage_comment: createTool({\n id: 'github_upsert_factory_triage_comment',\n description:\n 'Create or update this Factory App’s canonical triage handoff comment on an issue in the active repository. Use this for every marked pending or final Factory triage handoff; never use gh to create or edit that handoff.',\n inputSchema: triageCommentInputSchema,\n execute: async input => upsertFactoryTriageComment(requestContext, input, github),\n }),\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nexport function stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.toolName !== 'execute_command' || context.error) return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (\n typeof command !== 'string' ||\n !/(?:^|\\n|;|&&|\\|\\|)\\s*gh\\s+pr\\s+create(?:\\s|$)/.test(stripHeredocBodies(command))\n ) {\n return undefined;\n }\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWrD;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,4FAA4F;IAC5F,IAAI,EAAE,SAAS,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,KAAK,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAC;IAClE,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAkFD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CA6O/E"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWrD;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,4FAA4F;IAC5F,IAAI,EAAE,SAAS,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,KAAK,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAC;IAClE,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAqFD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CAwS/E"}
@@ -61,6 +61,8 @@ function parseAfterCursor(raw) {
61
61
  if (raw.length > 512 || !/^[\w+/=.:-]+$/.test(raw)) return null;
62
62
  return raw;
63
63
  }
64
+ /** Human issue key as it appears on a card (`ENG-123`). */
65
+ const ISSUE_IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9]{0,9}-\d{1,7}$/;
64
66
  /** Map a Linear read failure to the API response for the SPA. */
65
67
  function linearFetchError(c, err) {
66
68
  if (err instanceof LinearReauthRequiredError || err.status === 401) return c.json({
@@ -266,6 +268,54 @@ function buildLinearRoutes(options) {
266
268
  }
267
269
  }
268
270
  }));
271
+ routes.push(registerApiRoute("/web/linear/issues/:identifier", {
272
+ method: "GET",
273
+ requiresAuth: false,
274
+ handler: async (c) => {
275
+ const resolved = await resolveOrgTenant(loose(c), auth);
276
+ if ("response" in resolved) return resolved.response;
277
+ const identifier = c.req.param("identifier");
278
+ if (!ISSUE_IDENTIFIER_RE.test(identifier)) return c.json({ error: "invalid_identifier" }, 400);
279
+ const factoryProjectId = c.req.query("factoryProjectId");
280
+ if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) return c.json({ error: "invalid_factory_project_id" }, 400);
281
+ const connection = await linear.loadConnection(resolved.tenant.orgId);
282
+ if (!connection) return c.json({
283
+ error: "linear_not_connected",
284
+ message: "Connect Linear to see intake issues."
285
+ }, 409);
286
+ await intake.ensureReady();
287
+ const selection = (await intake.getConfig({
288
+ orgId: resolved.tenant.orgId,
289
+ userId: resolved.tenant.userId,
290
+ integrationIds: ["linear"]
291
+ })).linear;
292
+ if (!selection.enabled) return c.json({
293
+ error: "linear_intake_disabled",
294
+ message: "Linear intake is turned off in Settings."
295
+ }, 404);
296
+ const projectIds = await scopeSourceIdsToProject({
297
+ intake,
298
+ projects: options.projects,
299
+ orgId: resolved.tenant.orgId,
300
+ factoryProjectId,
301
+ selectedIds: selection.sourceIds ?? []
302
+ });
303
+ if (projectIds.length === 0) return c.json({ error: "issue_not_found" }, 404);
304
+ try {
305
+ const accessToken = await linear.getFreshAccessToken(connection);
306
+ const issue = await linear.fetchIssueDetail(accessToken, identifier);
307
+ if (!issue || issue.projectId === null || !projectIds.includes(issue.projectId)) return c.json({ error: "issue_not_found" }, 404);
308
+ return c.json({
309
+ identifier: issue.identifier,
310
+ title: issue.title,
311
+ url: issue.url,
312
+ description: issue.description
313
+ });
314
+ } catch (err) {
315
+ return linearFetchError(loose(c), err);
316
+ }
317
+ }
318
+ }));
269
319
  return routes;
270
320
  }
271
321
  //#endregion