@mastra/factory 0.13.0-alpha.9 → 0.13.0

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 (232) hide show
  1. package/README.md +232 -10
  2. package/dist/auth.d.ts +2 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +29 -4
  5. package/dist/auth.js.map +1 -1
  6. package/dist/boards/define-board.d.ts +36 -2
  7. package/dist/boards/define-board.d.ts.map +1 -1
  8. package/dist/boards/define-board.js +58 -1
  9. package/dist/boards/define-board.js.map +1 -1
  10. package/dist/boards/index.d.ts +4 -2
  11. package/dist/boards/index.d.ts.map +1 -1
  12. package/dist/boards/index.js +2 -1
  13. package/dist/boards/index.js.map +1 -1
  14. package/dist/boards/relocate.d.ts +17 -0
  15. package/dist/boards/relocate.d.ts.map +1 -0
  16. package/dist/boards/relocate.js +53 -0
  17. package/dist/boards/relocate.js.map +1 -0
  18. package/dist/boards/review.d.ts +1 -1
  19. package/dist/boards/review.d.ts.map +1 -1
  20. package/dist/boards/review.js +17 -4
  21. package/dist/boards/review.js.map +1 -1
  22. package/dist/boards/semantics.d.ts +28 -0
  23. package/dist/boards/semantics.d.ts.map +1 -0
  24. package/dist/boards/semantics.js +41 -0
  25. package/dist/boards/semantics.js.map +1 -0
  26. package/dist/boards/work-tool-rules.d.ts +8 -0
  27. package/dist/boards/work-tool-rules.d.ts.map +1 -0
  28. package/dist/boards/work-tool-rules.js +20 -0
  29. package/dist/boards/work-tool-rules.js.map +1 -0
  30. package/dist/boards/work.d.ts +1 -1
  31. package/dist/boards/work.d.ts.map +1 -1
  32. package/dist/boards/work.js +13 -0
  33. package/dist/boards/work.js.map +1 -1
  34. package/dist/capabilities/intake.d.ts +2 -0
  35. package/dist/capabilities/intake.d.ts.map +1 -1
  36. package/dist/factory.d.ts +5 -6
  37. package/dist/factory.d.ts.map +1 -1
  38. package/dist/factory.js +41 -12
  39. package/dist/factory.js.map +1 -1
  40. package/dist/integrations/base.d.ts +7 -4
  41. package/dist/integrations/base.d.ts.map +1 -1
  42. package/dist/integrations/github/default-rules.d.ts +2 -2
  43. package/dist/integrations/github/default-rules.d.ts.map +1 -1
  44. package/dist/integrations/github/default-rules.js +2 -2
  45. package/dist/integrations/github/default-rules.js.map +1 -1
  46. package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
  47. package/dist/integrations/github/issue-reconciler.js +4 -3
  48. package/dist/integrations/github/issue-reconciler.js.map +1 -1
  49. package/dist/integrations/github/rules.d.ts +6 -2
  50. package/dist/integrations/github/rules.d.ts.map +1 -1
  51. package/dist/integrations/github/rules.js +96 -15
  52. package/dist/integrations/github/rules.js.map +1 -1
  53. package/dist/integrations/github/sandbox.d.ts +6 -3
  54. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  55. package/dist/integrations/github/sandbox.js +21 -2
  56. package/dist/integrations/github/sandbox.js.map +1 -1
  57. package/dist/integrations/issue-reconciler.d.ts +2 -0
  58. package/dist/integrations/issue-reconciler.d.ts.map +1 -1
  59. package/dist/integrations/issue-reconciler.js +1 -3
  60. package/dist/integrations/issue-reconciler.js.map +1 -1
  61. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  62. package/dist/integrations/linear/agent-tools.js.map +1 -1
  63. package/dist/integrations/linear/default-rules.d.ts +2 -2
  64. package/dist/integrations/linear/default-rules.d.ts.map +1 -1
  65. package/dist/integrations/linear/default-rules.js +2 -2
  66. package/dist/integrations/linear/default-rules.js.map +1 -1
  67. package/dist/integrations/linear/integration.js +1 -0
  68. package/dist/integrations/linear/integration.js.map +1 -1
  69. package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
  70. package/dist/integrations/linear/issue-reconciler.js +9 -4
  71. package/dist/integrations/linear/issue-reconciler.js.map +1 -1
  72. package/dist/integrations/linear/reconciliation-config.d.ts.map +1 -1
  73. package/dist/integrations/linear/reconciliation-config.js.map +1 -1
  74. package/dist/integrations/linear/routes.d.ts.map +1 -1
  75. package/dist/integrations/linear/routes.js +19 -26
  76. package/dist/integrations/linear/routes.js.map +1 -1
  77. package/dist/integrations/linear/rules.d.ts +7 -2
  78. package/dist/integrations/linear/rules.d.ts.map +1 -1
  79. package/dist/integrations/linear/rules.js +20 -8
  80. package/dist/integrations/linear/rules.js.map +1 -1
  81. package/dist/integrations/platform/api-client.d.ts.map +1 -1
  82. package/dist/integrations/platform/api-client.js +25 -4
  83. package/dist/integrations/platform/api-client.js.map +1 -1
  84. package/dist/integrations/platform/github/integration.d.ts +1 -0
  85. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  86. package/dist/integrations/platform/github/integration.js +5 -0
  87. package/dist/integrations/platform/github/integration.js.map +1 -1
  88. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  89. package/dist/integrations/platform/linear/integration.js +5 -2
  90. package/dist/integrations/platform/linear/integration.js.map +1 -1
  91. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts +8 -0
  92. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts.map +1 -0
  93. package/dist/integrations/slack/emoji-shortcodes.generated.js +1985 -0
  94. package/dist/integrations/slack/emoji-shortcodes.generated.js.map +1 -0
  95. package/dist/integrations/slack/emoji.d.ts +3 -0
  96. package/dist/integrations/slack/emoji.d.ts.map +1 -0
  97. package/dist/integrations/slack/emoji.js +12 -0
  98. package/dist/integrations/slack/emoji.js.map +1 -0
  99. package/dist/integrations/slack/integration.js +1 -1
  100. package/dist/integrations/slack/integration.js.map +1 -1
  101. package/dist/integrations/slack/slack.d.ts.map +1 -1
  102. package/dist/integrations/slack/slack.js +16 -2
  103. package/dist/integrations/slack/slack.js.map +1 -1
  104. package/dist/routes/attention-activity.d.ts +2 -3
  105. package/dist/routes/attention-activity.d.ts.map +1 -1
  106. package/dist/routes/attention-activity.js +12 -3
  107. package/dist/routes/attention-activity.js.map +1 -1
  108. package/dist/routes/attention-parked.d.ts +22 -0
  109. package/dist/routes/attention-parked.d.ts.map +1 -0
  110. package/dist/routes/attention-parked.js +136 -0
  111. package/dist/routes/attention-parked.js.map +1 -0
  112. package/dist/routes/attention-providers.js +8 -8
  113. package/dist/routes/attention-providers.js.map +1 -1
  114. package/dist/routes/attention.d.ts +2 -0
  115. package/dist/routes/attention.d.ts.map +1 -1
  116. package/dist/routes/attention.js +47 -118
  117. package/dist/routes/attention.js.map +1 -1
  118. package/dist/routes/contracts.d.ts +627 -0
  119. package/dist/routes/contracts.d.ts.map +1 -0
  120. package/dist/routes/contracts.js +468 -0
  121. package/dist/routes/contracts.js.map +1 -0
  122. package/dist/routes/intake.d.ts +18 -0
  123. package/dist/routes/intake.d.ts.map +1 -1
  124. package/dist/routes/intake.js +265 -15
  125. package/dist/routes/intake.js.map +1 -1
  126. package/dist/routes/projects.d.ts.map +1 -1
  127. package/dist/routes/projects.js +26 -76
  128. package/dist/routes/projects.js.map +1 -1
  129. package/dist/routes/supervisor.d.ts +2 -0
  130. package/dist/routes/supervisor.d.ts.map +1 -1
  131. package/dist/routes/supervisor.js +7 -6
  132. package/dist/routes/supervisor.js.map +1 -1
  133. package/dist/routes/surface.d.ts +7 -4
  134. package/dist/routes/surface.d.ts.map +1 -1
  135. package/dist/routes/surface.js +17 -11
  136. package/dist/routes/surface.js.map +1 -1
  137. package/dist/routes/telemetry.d.ts +13 -0
  138. package/dist/routes/telemetry.d.ts.map +1 -0
  139. package/dist/routes/telemetry.js +60 -0
  140. package/dist/routes/telemetry.js.map +1 -0
  141. package/dist/routes/work-items.d.ts +3 -3
  142. package/dist/routes/work-items.d.ts.map +1 -1
  143. package/dist/routes/work-items.js +134 -250
  144. package/dist/routes/work-items.js.map +1 -1
  145. package/dist/rules/dispatch-errors.d.ts +2 -2
  146. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  147. package/dist/rules/dispatch-errors.js +10 -6
  148. package/dist/rules/dispatch-errors.js.map +1 -1
  149. package/dist/rules/dispatcher.d.ts +9 -2
  150. package/dist/rules/dispatcher.d.ts.map +1 -1
  151. package/dist/rules/dispatcher.js +76 -23
  152. package/dist/rules/dispatcher.js.map +1 -1
  153. package/dist/rules/index.d.ts +4 -5
  154. package/dist/rules/index.d.ts.map +1 -1
  155. package/dist/rules/index.js +3 -4
  156. package/dist/rules/processor.d.ts +3 -3
  157. package/dist/rules/processor.d.ts.map +1 -1
  158. package/dist/rules/processor.js +24 -31
  159. package/dist/rules/processor.js.map +1 -1
  160. package/dist/rules/resolve.d.ts +1 -3
  161. package/dist/rules/resolve.d.ts.map +1 -1
  162. package/dist/rules/resolve.js +1 -4
  163. package/dist/rules/resolve.js.map +1 -1
  164. package/dist/rules/start-coordinator.d.ts.map +1 -1
  165. package/dist/rules/start-coordinator.js +3 -1
  166. package/dist/rules/start-coordinator.js.map +1 -1
  167. package/dist/rules/tools.d.ts.map +1 -1
  168. package/dist/rules/tools.js +20 -10
  169. package/dist/rules/tools.js.map +1 -1
  170. package/dist/rules/transition-service.d.ts +7 -7
  171. package/dist/rules/transition-service.d.ts.map +1 -1
  172. package/dist/rules/transition-service.js +21 -25
  173. package/dist/rules/transition-service.js.map +1 -1
  174. package/dist/rules/types.d.ts +16 -11
  175. package/dist/rules/types.d.ts.map +1 -1
  176. package/dist/rules/types.js.map +1 -1
  177. package/dist/rules/validation.d.ts +12 -2
  178. package/dist/rules/validation.d.ts.map +1 -1
  179. package/dist/rules/validation.js +29 -20
  180. package/dist/rules/validation.js.map +1 -1
  181. package/dist/session/live-sessions.d.ts +33 -3
  182. package/dist/session/live-sessions.d.ts.map +1 -1
  183. package/dist/session/live-sessions.js +67 -5
  184. package/dist/session/live-sessions.js.map +1 -1
  185. package/dist/storage/domains/intake/base.d.ts +54 -1
  186. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  187. package/dist/storage/domains/intake/base.js +136 -4
  188. package/dist/storage/domains/intake/base.js.map +1 -1
  189. package/dist/storage/domains/work-items/base.d.ts +22 -7
  190. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  191. package/dist/storage/domains/work-items/base.js +41 -26
  192. package/dist/storage/domains/work-items/base.js.map +1 -1
  193. package/dist/supervisor/health-worker.d.ts +2 -0
  194. package/dist/supervisor/health-worker.d.ts.map +1 -1
  195. package/dist/supervisor/health-worker.js +3 -1
  196. package/dist/supervisor/health-worker.js.map +1 -1
  197. package/dist/supervisor/health.d.ts +12 -14
  198. package/dist/supervisor/health.d.ts.map +1 -1
  199. package/dist/supervisor/health.js +24 -52
  200. package/dist/supervisor/health.js.map +1 -1
  201. package/dist/supervisor/instructions.d.ts +1 -1
  202. package/dist/supervisor/instructions.d.ts.map +1 -1
  203. package/dist/supervisor/instructions.js +10 -8
  204. package/dist/supervisor/instructions.js.map +1 -1
  205. package/dist/supervisor/read-tools.d.ts +2 -0
  206. package/dist/supervisor/read-tools.d.ts.map +1 -1
  207. package/dist/supervisor/read-tools.js +2 -2
  208. package/dist/supervisor/read-tools.js.map +1 -1
  209. package/dist/supervisor/write-tools.d.ts.map +1 -1
  210. package/dist/supervisor/write-tools.js +6 -4
  211. package/dist/supervisor/write-tools.js.map +1 -1
  212. package/dist/telemetry-types.d.ts +7 -0
  213. package/dist/telemetry-types.d.ts.map +1 -0
  214. package/dist/telemetry-types.js +24 -0
  215. package/dist/telemetry-types.js.map +1 -0
  216. package/dist/telemetry.d.ts +9 -0
  217. package/dist/telemetry.d.ts.map +1 -0
  218. package/dist/telemetry.js +47 -0
  219. package/dist/telemetry.js.map +1 -0
  220. package/dist/work-item-branch.d.ts +2 -0
  221. package/dist/work-item-branch.d.ts.map +1 -1
  222. package/dist/work-item-branch.js +6 -1
  223. package/dist/work-item-branch.js.map +1 -1
  224. package/dist/workspace.d.ts.map +1 -1
  225. package/dist/workspace.js +3 -1
  226. package/dist/workspace.js.map +1 -1
  227. package/factory-skills/configure-factory-rules/SKILL.md +30 -10
  228. package/package.json +12 -10
  229. package/dist/rules/defaults.d.ts +0 -9
  230. package/dist/rules/defaults.d.ts.map +0 -1
  231. package/dist/rules/defaults.js +0 -50
  232. package/dist/rules/defaults.js.map +0 -1
@@ -11,12 +11,12 @@ declare function retriageGithubIssue(context: FactoryGithubRuleContext): {
11
11
  declare function issueOpened(context: FactoryGithubRuleContext): {
12
12
  readonly type: "upsertLinkedWorkItem";
13
13
  readonly idempotencyKey: `${string}:issue-intake`;
14
- readonly board: "work";
14
+ readonly board: string;
15
15
  readonly source: "github-issue";
16
16
  readonly sourceKey: `github-issue:${number}`;
17
17
  readonly title: string;
18
18
  readonly url: string;
19
- readonly stage: "intake";
19
+ readonly stage: string;
20
20
  readonly metadata: {
21
21
  readonly authorTrusted: boolean;
22
22
  readonly autoStartCandidate: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"default-rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/default-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEjH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CAAC,sBAAsB,EAAE,kBAAkB,CAAC,wBAAwB,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAChG,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CACrC,MAAM,CAAC,sBAAsB,EAAE,kBAAkB,CAAC,wBAAwB,CAAC,GAAG,IAAI,CAAC,CACpF,CAAC;AAUF,iBAAS,mBAAmB,CAAC,OAAO,EAAE,wBAAwB;;;;;;cAuB7D;AASD,iBAAS,WAAW,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;cAyBrD;AAED,iBAAS,WAAW,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;cAqBrD;AAoCD,iBAAS,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;cAW3D;AAED,iBAAS,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;cA6B3D;AAED,iBAAS,qBAAqB,CAAC,OAAO,EAAE,wBAAwB;;;;;;cA0B/D;AA4BD,iBAAS,yBAAyB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCnE;AAED,iBAAS,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;cAgB3D;AAED,iBAAS,4BAA4B,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6BtE;AAED,iBAAS,0BAA0B,CAAC,OAAO,EAAE,wBAAwB;;;;;;cAoBpE;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;EAcF,CAAC;AAE9B,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,mBAAmB,GAAG,gBAAgB,CAgBpF"}
1
+ {"version":3,"file":"default-rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/default-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEjH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CAAC,sBAAsB,EAAE,kBAAkB,CAAC,wBAAwB,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAChG,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CACrC,MAAM,CAAC,sBAAsB,EAAE,kBAAkB,CAAC,wBAAwB,CAAC,GAAG,IAAI,CAAC,CACpF,CAAC;AAUF,iBAAS,mBAAmB,CAAC,OAAO,EAAE,wBAAwB;;;;;;cAuB7D;AASD,iBAAS,WAAW,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;cA0BrD;AAED,iBAAS,WAAW,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;cAqBrD;AAoCD,iBAAS,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;cAW3D;AAED,iBAAS,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;cA6B3D;AAED,iBAAS,qBAAqB,CAAC,OAAO,EAAE,wBAAwB;;;;;;cA0B/D;AA4BD,iBAAS,yBAAyB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCnE;AAED,iBAAS,iBAAiB,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;cAgB3D;AAED,iBAAS,4BAA4B,CAAC,OAAO,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6BtE;AAED,iBAAS,0BAA0B,CAAC,OAAO,EAAE,wBAAwB;;;;;;cAoBpE;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;EAcF,CAAC;AAE9B,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,mBAAmB,GAAG,gBAAgB,CAgBpF"}
@@ -28,12 +28,12 @@ function issueOpened(context) {
28
28
  return {
29
29
  type: "upsertLinkedWorkItem",
30
30
  idempotencyKey: `${context.ingress.id}:issue-intake`,
31
- board: "work",
31
+ board: context.intake?.board ?? "work",
32
32
  source: "github-issue",
33
33
  sourceKey: `github-issue:${context.issue.number}`,
34
34
  title: context.issue.title,
35
35
  url: context.issue.url,
36
- stage: "intake",
36
+ stage: context.intake?.initialPhase ?? "intake",
37
37
  metadata: {
38
38
  githubRepositoryId: context.repository.id,
39
39
  githubIssueNumber: context.issue.number,
@@ -1 +1 @@
1
- {"version":3,"file":"default-rules.js","names":[],"sources":["../../../src/integrations/github/default-rules.ts"],"sourcesContent":["import type { FactoryGithubEventName, FactoryGithubRuleContext, FactoryRuleHandler } from '../../rules/types.js';\n\nexport type GithubRuleOverrides = Partial<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null | undefined>\n>;\nexport type GithubEventRules = Readonly<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null>\n>;\n\nfunction trustedGithubActor(context: Pick<FactoryGithubRuleContext, 'actor'>): boolean {\n return context.actor.type === 'github' && context.actor.trusted;\n}\n\nfunction githubActorLogin(context: Pick<FactoryGithubRuleContext, 'actor'>): string | undefined {\n return context.actor.type === 'github' ? context.actor.login : undefined;\n}\n\nfunction retriageGithubIssue(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.item.url) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n\n const reason =\n context.event === 'issueEdited'\n ? context.issueChange?.title && context.issueChange.body\n ? 'issue title and body edited'\n : context.issueChange?.title\n ? 'issue title edited'\n : 'issue body edited'\n : context.event === 'issueCommentDeleted'\n ? 'comment deleted'\n : context.event === 'issueCommentEdited'\n ? 'comment edited'\n : 'comment created';\n return {\n type: 'invokeSkill',\n idempotencyKey: `${context.ingress.id}:factory-triage`,\n role: 'triage',\n skillName: 'factory-triage',\n arguments: `Re-triage GitHub issue (${context.item.url}) after ${reason}.`,\n } as const;\n}\n\nfunction createdAfterFactory(createdAt: string | undefined, factoryCreatedAt: string): boolean {\n if (!createdAt) return false;\n const sourceCreatedAt = Date.parse(createdAt);\n const projectCreatedAt = Date.parse(factoryCreatedAt);\n return Number.isFinite(sourceCreatedAt) && Number.isFinite(projectCreatedAt) && sourceCreatedAt > projectCreatedAt;\n}\n\nfunction issueOpened(context: FactoryGithubRuleContext) {\n if (!context.issue) return;\n // Everything arrives in Intake; arrival only stamps whether `onArrival` may\n // suggest this card's run without a person.\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey: `${context.ingress.id}:issue-intake`,\n board: 'work',\n source: 'github-issue',\n sourceKey: `github-issue:${context.issue.number}`,\n title: context.issue.title,\n url: context.issue.url,\n stage: 'intake',\n metadata: {\n githubRepositoryId: context.repository.id,\n githubIssueNumber: context.issue.number,\n ...(context.issue.createdAt ? { sourceCreatedAt: context.issue.createdAt } : {}),\n ...(githubActorLogin(context) ? { author: githubActorLogin(context) } : {}),\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate:\n trustedGithubActor(context) && createdAfterFactory(context.issue.createdAt, context.factory.createdAt),\n assignees: context.issue.assignees ?? [],\n labels: context.issue.labels ?? [],\n },\n } as const;\n}\n\nfunction issueClosed(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.issue) return;\n if (context.board !== 'work') return;\n // Already off the board: nothing to reconcile.\n if (context.item.stages.some(stage => stage === 'done' || stage === 'canceled')) return;\n // Issue closure is a repository fact, not third-party input — no actor trust\n // gate. `not_planned` (and `duplicate`) means abandoned, everything else is\n // completed work.\n const canceled = context.issue.stateReason === 'not_planned' || context.issue.stateReason === 'duplicate';\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:issue-closed`,\n board: 'work',\n stage: canceled ? 'canceled' : 'done',\n message: {\n text:\n `GitHub issue #${context.issue.number} was closed` +\n `${context.issue.stateReason ? ` (${context.issue.stateReason})` : ''}; ` +\n `this Work card was moved to ${canceled ? 'Canceled' : 'Done'}.`,\n },\n } as const;\n}\n\nfunction materializePullRequestIntake(\n context: FactoryGithubRuleContext,\n { idempotencyKey, autoStartCandidate }: { idempotencyKey: string; autoStartCandidate: boolean },\n) {\n if (!context.pullRequest) return;\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey,\n board: 'review',\n source: 'github-pr',\n sourceKey: `github-pr:${context.pullRequest.number}`,\n title: context.pullRequest.title,\n url: context.pullRequest.url,\n stage: 'intake',\n metadata: {\n githubRepositoryId: context.repository.id,\n githubPullRequestNumber: context.pullRequest.number,\n ...(context.pullRequest.createdAt ? { sourceCreatedAt: context.pullRequest.createdAt } : {}),\n factoryAuthored: context.pullRequest.factoryAuthored,\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate,\n state: context.pullRequest.state,\n draft: context.pullRequest.draft,\n merged: context.pullRequest.merged,\n assignees: context.pullRequest.assignees ?? [],\n requestedReviewers: context.pullRequest.requestedReviewers ?? [],\n labels: context.pullRequest.labels ?? [],\n headBranch: context.pullRequest.headBranch,\n baseBranch: context.pullRequest.baseBranch,\n ...(context.pullRequest.author ? { author: context.pullRequest.author } : {}),\n },\n } as const;\n}\n\nfunction pullRequestOpened(context: FactoryGithubRuleContext) {\n if (!context.pullRequest) return;\n // A GitHub App bot is never a collaborator, so Factory's own PRs score\n // untrusted; their authorship is the trust signal.\n const autoStartCandidate =\n (trustedGithubActor(context) || context.pullRequest.factoryAuthored) &&\n createdAfterFactory(context.pullRequest.createdAt, context.factory.createdAt);\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-intake`,\n autoStartCandidate,\n });\n}\n\nfunction pullRequestMerged(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest?.merged) return;\n if (context.board === 'review') {\n // The event is bound to the PR's own Review card: a merged PR is finished\n // review work, so always move the card to Done. The message only reaches\n // an active session (if any) — cards without one just move, instead of\n // failing retries against a binding that never existed.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-merged`,\n board: 'review',\n stage: 'done',\n message: {\n text:\n `Pull request #${context.pullRequest.number} merged; this Review card was moved to Done. ` +\n 'No further review is needed unless follow-up work was requested.',\n },\n } as const;\n }\n // Provenance bound the event to the originating Work item instead: remind\n // its agent to assess completion — never auto-complete the Work item.\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:assess-work-completion`,\n role: 'work',\n message:\n `Pull request #${context.pullRequest.number} merged. Assess whether the linked Work item is complete. ` +\n 'Do not mark it Done solely because this PR merged; use factory_transition_work_item only after verifying the work.',\n } as const;\n}\n\nfunction addressReviewFeedback(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || !context.review) return;\n // Only the Work item that authored the PR can act on the feedback. Provenance\n // binds the event there; a Review card seeing its own posted review must not\n // react to it (that would loop the reviewer against itself).\n if (context.board !== 'work') return;\n // A closed or merged pull request has no branch left to push fixes to.\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `approved` needs no work, and `commented` (a review body with no verdict)\n // is how a reviewer leaves notes without blocking — only a verdict that asks\n // for changes should pull the author back in.\n if (context.review.state.toLowerCase() !== 'changes_requested') return;\n // The authoring thread is already subscribed to this PR (`gh pr create`\n // subscribes automatically), so it can read the individual line comments\n // from its own notification inbox — the message only has to wake it and\n // point at the review.\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-review-feedback`,\n role: 'work',\n priority: 'high',\n message:\n `Changes were requested on pull request #${context.pullRequest.number} (${context.review.url}). ` +\n 'Read the review comments on this PR, address the ones you agree with, and push the fixes to the PR branch. ' +\n 'Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\n/**\n * Detects the `factory-review` handoff verdict in a comment body.\n *\n * GitHub forbids an app from reviewing a pull request it authored, so on\n * Factory-authored PRs the review skill falls back to posting its verdict as a\n * plain comment. That comment is the only signal the authoring agent gets, so\n * it has to be readable back out. The skill's handoff contract puts the verdict\n * on the first line (`Verdict: request changes`), so only that line is\n * inspected — a verdict quoted later in the findings must not count.\n */\nfunction requestsChangesVerdict(body: string | undefined): boolean {\n const firstLine = body\n ?.split('\\n')\n .map(line => line.trim())\n .find(line => line.length > 0);\n if (!firstLine) return false;\n // Tolerate the markdown the skill wraps the line in (`**Verdict: ...**`).\n const normalized = firstLine\n .replaceAll(/[*_`#>\\s]+/g, ' ')\n .trim()\n .toLowerCase();\n // Match the verdict exactly so negated phrasings (\"Verdict: do not request\n // changes\") cannot wake the author.\n return /^verdict: ?(request changes|changes requested)$/.test(normalized);\n}\n\nfunction addressPullRequestComment(context: FactoryGithubRuleContext) {\n // A validated Factory mention is a review-entry request, not feedback for the\n // authoring Work session. Invalid or unrecognized comments retain the normal\n // feedback route below.\n if (context.reviewCommand) return reReviewRequestedPullRequest(context);\n if (!context.item || !context.pullRequest || !context.issueComment) return;\n // Provenance binds the comment to the Work item that authored the PR — the\n // only session that can act on it. A Review card must not react to comments\n // on the PR it is reviewing.\n if (context.board !== 'work') return;\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `factoryAuthored` is one bit for the whole Factory, so Factory's own\n // comments are indistinguishable between roles — waking on all of them would\n // let the Work agent's own progress comments wake itself in a loop. The one\n // exception is the review verdict the Review run had to post as a comment\n // because GitHub refused a self-review: that is the handoff, and it only ever\n // asks for changes once per review, so it cannot sustain a loop.\n if (\n context.actor.type === 'github' &&\n context.actor.factoryAuthored &&\n !requestsChangesVerdict(context.issueComment.body)\n ) {\n return;\n }\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-pull-request-comment`,\n role: 'work',\n priority: 'high',\n message:\n `${context.issueComment.author ?? 'Someone'} commented on pull request #${context.pullRequest.number} ` +\n `(${context.issueComment.url ?? context.pullRequest.url}). Read the comment, address it if you agree, and push ` +\n 'the fixes to the PR branch. Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\nfunction pullRequestClosed(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || context.pullRequest.merged) return;\n if (context.board !== 'review') return;\n // A PR closed without merging is abandoned review work: clear the card off\n // the board instead of leaving it in Reviewing forever.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-closed`,\n board: 'review',\n stage: 'canceled',\n message: {\n text:\n `Pull request #${context.pullRequest.number} was closed without merging; ` +\n 'this Review card was moved to Canceled.',\n },\n } as const;\n}\n\nfunction reReviewRequestedPullRequest(context: FactoryGithubRuleContext) {\n // GitHub reviewer requests and Factory's exact mention command are both\n // explicit requests to enter the same Review lifecycle.\n const factoryReviewEntry = context.reviewRequest?.factoryReviewer || context.reviewCommand !== undefined;\n if ((context.item && context.board !== 'review') || !factoryReviewEntry) return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Trusted (write/admin) requesters only: creating or re-entering review checks\n // out and executes PR code, the same bar pullRequestOpened applies to auto-review.\n if (!trustedGithubActor(context)) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n if (!context.item) {\n // On this path the actor is the *requester*, so the materialized\n // `authorTrusted` stamp records their trust (always true past the gate\n // above), not the PR author's: a trusted maintainer requesting a Factory\n // review vouches for the PR.\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-review-requested-intake`,\n autoStartCandidate: true,\n });\n }\n // Already in Reviewing: a review pass is pending or running; re-entering\n // would be a same-stage no-op anyway (stage rules only fire on change).\n if (context.item.stages.length === 1 && context.item.stages[0] === 'review') return;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-requested`,\n board: 'review',\n stage: 'review',\n } as const;\n}\n\nfunction reReviewUpdatedPullRequest(context: FactoryGithubRuleContext) {\n if (!context.item || context.board !== 'review') return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Intake has not started a review pass yet, so a push there is just more of\n // the code the first pass will read. A push to a card sitting in Reviewing is\n // different: it invalidates whatever that pass is reading, so re-enter the\n // stage to supersede it. `reviewPullRequest` cancels the stale run and picks\n // the right skill for the entry it sees.\n if (context.item.stages.some(stage => stage === 'intake')) return;\n const alreadyReviewing = context.item.stages.some(stage => stage === 'review');\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-updated`,\n board: 'review',\n stage: 'review',\n // Re-entry is the point when the card is already Reviewing: the stage's\n // entry rule is what cancels the superseded pass and starts one on the code\n // that just landed.\n ...(alreadyReviewing ? { reenter: true } : {}),\n } as const;\n}\n\nexport const defaultGithubRules = Object.freeze({\n issueOpened: issueOpened,\n issueEdited: retriageGithubIssue,\n issueClosed: issueClosed,\n issueCommentCreated: retriageGithubIssue,\n issueCommentEdited: retriageGithubIssue,\n issueCommentDeleted: retriageGithubIssue,\n pullRequestOpened: pullRequestOpened,\n pullRequestUpdated: reReviewUpdatedPullRequest,\n pullRequestCommentCreated: addressPullRequestComment,\n pullRequestReviewRequested: reReviewRequestedPullRequest,\n pullRequestReviewSubmitted: addressReviewFeedback,\n pullRequestMerged: pullRequestMerged,\n pullRequestClosed: pullRequestClosed,\n} satisfies GithubEventRules);\n\nexport function resolveGithubRules(overrides?: GithubRuleOverrides): GithubEventRules {\n if (overrides !== undefined && (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides))) {\n throw new Error('GitHub rules must be an object.');\n }\n const rules: Record<string, FactoryRuleHandler<FactoryGithubRuleContext> | null> = { ...defaultGithubRules };\n for (const key of Reflect.ownKeys(overrides ?? {})) {\n if (typeof key !== 'string' || !Object.hasOwn(defaultGithubRules, key)) {\n throw new Error(`Unknown GitHub rule event: ${String(key)}.`);\n }\n const handler = overrides?.[key as FactoryGithubEventName];\n if (handler !== undefined && handler !== null && typeof handler !== 'function') {\n throw new Error(`GitHub rule ${key} must be a function, null, or undefined.`);\n }\n if (handler !== undefined) rules[key] = handler;\n }\n return Object.freeze(rules) as GithubEventRules;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"default-rules.js","names":[],"sources":["../../../src/integrations/github/default-rules.ts"],"sourcesContent":["import type { FactoryGithubEventName, FactoryGithubRuleContext, FactoryRuleHandler } from '../../rules/types.js';\n\nexport type GithubRuleOverrides = Partial<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null | undefined>\n>;\nexport type GithubEventRules = Readonly<\n Record<FactoryGithubEventName, FactoryRuleHandler<FactoryGithubRuleContext> | null>\n>;\n\nfunction trustedGithubActor(context: Pick<FactoryGithubRuleContext, 'actor'>): boolean {\n return context.actor.type === 'github' && context.actor.trusted;\n}\n\nfunction githubActorLogin(context: Pick<FactoryGithubRuleContext, 'actor'>): string | undefined {\n return context.actor.type === 'github' ? context.actor.login : undefined;\n}\n\nfunction retriageGithubIssue(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.item.url) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n\n const reason =\n context.event === 'issueEdited'\n ? context.issueChange?.title && context.issueChange.body\n ? 'issue title and body edited'\n : context.issueChange?.title\n ? 'issue title edited'\n : 'issue body edited'\n : context.event === 'issueCommentDeleted'\n ? 'comment deleted'\n : context.event === 'issueCommentEdited'\n ? 'comment edited'\n : 'comment created';\n return {\n type: 'invokeSkill',\n idempotencyKey: `${context.ingress.id}:factory-triage`,\n role: 'triage',\n skillName: 'factory-triage',\n arguments: `Re-triage GitHub issue (${context.item.url}) after ${reason}.`,\n } as const;\n}\n\nfunction createdAfterFactory(createdAt: string | undefined, factoryCreatedAt: string): boolean {\n if (!createdAt) return false;\n const sourceCreatedAt = Date.parse(createdAt);\n const projectCreatedAt = Date.parse(factoryCreatedAt);\n return Number.isFinite(sourceCreatedAt) && Number.isFinite(projectCreatedAt) && sourceCreatedAt > projectCreatedAt;\n}\n\nfunction issueOpened(context: FactoryGithubRuleContext) {\n if (!context.issue) return;\n // Everything arrives on the routed board's initial phase (Work › Intake when\n // no label route selects another board); arrival only stamps whether\n // `onArrival` may suggest this card's run without a person.\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey: `${context.ingress.id}:issue-intake`,\n board: context.intake?.board ?? 'work',\n source: 'github-issue',\n sourceKey: `github-issue:${context.issue.number}`,\n title: context.issue.title,\n url: context.issue.url,\n stage: context.intake?.initialPhase ?? 'intake',\n metadata: {\n githubRepositoryId: context.repository.id,\n githubIssueNumber: context.issue.number,\n ...(context.issue.createdAt ? { sourceCreatedAt: context.issue.createdAt } : {}),\n ...(githubActorLogin(context) ? { author: githubActorLogin(context) } : {}),\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate:\n trustedGithubActor(context) && createdAfterFactory(context.issue.createdAt, context.factory.createdAt),\n assignees: context.issue.assignees ?? [],\n labels: context.issue.labels ?? [],\n },\n } as const;\n}\n\nfunction issueClosed(context: FactoryGithubRuleContext) {\n if (!context.item || context.item.source !== 'github-issue' || !context.issue) return;\n if (context.board !== 'work') return;\n // Already off the board: nothing to reconcile.\n if (context.item.stages.some(stage => stage === 'done' || stage === 'canceled')) return;\n // Issue closure is a repository fact, not third-party input — no actor trust\n // gate. `not_planned` (and `duplicate`) means abandoned, everything else is\n // completed work.\n const canceled = context.issue.stateReason === 'not_planned' || context.issue.stateReason === 'duplicate';\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:issue-closed`,\n board: 'work',\n stage: canceled ? 'canceled' : 'done',\n message: {\n text:\n `GitHub issue #${context.issue.number} was closed` +\n `${context.issue.stateReason ? ` (${context.issue.stateReason})` : ''}; ` +\n `this Work card was moved to ${canceled ? 'Canceled' : 'Done'}.`,\n },\n } as const;\n}\n\nfunction materializePullRequestIntake(\n context: FactoryGithubRuleContext,\n { idempotencyKey, autoStartCandidate }: { idempotencyKey: string; autoStartCandidate: boolean },\n) {\n if (!context.pullRequest) return;\n return {\n type: 'upsertLinkedWorkItem',\n idempotencyKey,\n board: 'review',\n source: 'github-pr',\n sourceKey: `github-pr:${context.pullRequest.number}`,\n title: context.pullRequest.title,\n url: context.pullRequest.url,\n stage: 'intake',\n metadata: {\n githubRepositoryId: context.repository.id,\n githubPullRequestNumber: context.pullRequest.number,\n ...(context.pullRequest.createdAt ? { sourceCreatedAt: context.pullRequest.createdAt } : {}),\n factoryAuthored: context.pullRequest.factoryAuthored,\n authorTrusted: trustedGithubActor(context),\n autoStartCandidate,\n state: context.pullRequest.state,\n draft: context.pullRequest.draft,\n merged: context.pullRequest.merged,\n assignees: context.pullRequest.assignees ?? [],\n requestedReviewers: context.pullRequest.requestedReviewers ?? [],\n labels: context.pullRequest.labels ?? [],\n headBranch: context.pullRequest.headBranch,\n baseBranch: context.pullRequest.baseBranch,\n ...(context.pullRequest.author ? { author: context.pullRequest.author } : {}),\n },\n } as const;\n}\n\nfunction pullRequestOpened(context: FactoryGithubRuleContext) {\n if (!context.pullRequest) return;\n // A GitHub App bot is never a collaborator, so Factory's own PRs score\n // untrusted; their authorship is the trust signal.\n const autoStartCandidate =\n (trustedGithubActor(context) || context.pullRequest.factoryAuthored) &&\n createdAfterFactory(context.pullRequest.createdAt, context.factory.createdAt);\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-intake`,\n autoStartCandidate,\n });\n}\n\nfunction pullRequestMerged(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest?.merged) return;\n if (context.board === 'review') {\n // The event is bound to the PR's own Review card: a merged PR is finished\n // review work, so always move the card to Done. The message only reaches\n // an active session (if any) — cards without one just move, instead of\n // failing retries against a binding that never existed.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-merged`,\n board: 'review',\n stage: 'done',\n message: {\n text:\n `Pull request #${context.pullRequest.number} merged; this Review card was moved to Done. ` +\n 'No further review is needed unless follow-up work was requested.',\n },\n } as const;\n }\n // Provenance bound the event to the originating Work item instead: remind\n // its agent to assess completion — never auto-complete the Work item.\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:assess-work-completion`,\n role: 'work',\n message:\n `Pull request #${context.pullRequest.number} merged. Assess whether the linked Work item is complete. ` +\n 'Do not mark it Done solely because this PR merged; use factory_transition_work_item only after verifying the work.',\n } as const;\n}\n\nfunction addressReviewFeedback(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || !context.review) return;\n // Only the Work item that authored the PR can act on the feedback. Provenance\n // binds the event there; a Review card seeing its own posted review must not\n // react to it (that would loop the reviewer against itself).\n if (context.board !== 'work') return;\n // A closed or merged pull request has no branch left to push fixes to.\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `approved` needs no work, and `commented` (a review body with no verdict)\n // is how a reviewer leaves notes without blocking — only a verdict that asks\n // for changes should pull the author back in.\n if (context.review.state.toLowerCase() !== 'changes_requested') return;\n // The authoring thread is already subscribed to this PR (`gh pr create`\n // subscribes automatically), so it can read the individual line comments\n // from its own notification inbox — the message only has to wake it and\n // point at the review.\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-review-feedback`,\n role: 'work',\n priority: 'high',\n message:\n `Changes were requested on pull request #${context.pullRequest.number} (${context.review.url}). ` +\n 'Read the review comments on this PR, address the ones you agree with, and push the fixes to the PR branch. ' +\n 'Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\n/**\n * Detects the `factory-review` handoff verdict in a comment body.\n *\n * GitHub forbids an app from reviewing a pull request it authored, so on\n * Factory-authored PRs the review skill falls back to posting its verdict as a\n * plain comment. That comment is the only signal the authoring agent gets, so\n * it has to be readable back out. The skill's handoff contract puts the verdict\n * on the first line (`Verdict: request changes`), so only that line is\n * inspected — a verdict quoted later in the findings must not count.\n */\nfunction requestsChangesVerdict(body: string | undefined): boolean {\n const firstLine = body\n ?.split('\\n')\n .map(line => line.trim())\n .find(line => line.length > 0);\n if (!firstLine) return false;\n // Tolerate the markdown the skill wraps the line in (`**Verdict: ...**`).\n const normalized = firstLine\n .replaceAll(/[*_`#>\\s]+/g, ' ')\n .trim()\n .toLowerCase();\n // Match the verdict exactly so negated phrasings (\"Verdict: do not request\n // changes\") cannot wake the author.\n return /^verdict: ?(request changes|changes requested)$/.test(normalized);\n}\n\nfunction addressPullRequestComment(context: FactoryGithubRuleContext) {\n // A validated Factory mention is a review-entry request, not feedback for the\n // authoring Work session. Invalid or unrecognized comments retain the normal\n // feedback route below.\n if (context.reviewCommand) return reReviewRequestedPullRequest(context);\n if (!context.item || !context.pullRequest || !context.issueComment) return;\n // Provenance binds the comment to the Work item that authored the PR — the\n // only session that can act on it. A Review card must not react to comments\n // on the PR it is reviewing.\n if (context.board !== 'work') return;\n if (context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // `factoryAuthored` is one bit for the whole Factory, so Factory's own\n // comments are indistinguishable between roles — waking on all of them would\n // let the Work agent's own progress comments wake itself in a loop. The one\n // exception is the review verdict the Review run had to post as a comment\n // because GitHub refused a self-review: that is the handoff, and it only ever\n // asks for changes once per review, so it cannot sustain a loop.\n if (\n context.actor.type === 'github' &&\n context.actor.factoryAuthored &&\n !requestsChangesVerdict(context.issueComment.body)\n ) {\n return;\n }\n return {\n type: 'sendMessage',\n idempotencyKey: `${context.ingress.id}:address-pull-request-comment`,\n role: 'work',\n priority: 'high',\n message:\n `${context.issueComment.author ?? 'Someone'} commented on pull request #${context.pullRequest.number} ` +\n `(${context.issueComment.url ?? context.pullRequest.url}). Read the comment, address it if you agree, and push ` +\n 'the fixes to the PR branch. Reply on GitHub to anything you are deliberately not changing, explaining why.',\n } as const;\n}\n\nfunction pullRequestClosed(context: FactoryGithubRuleContext) {\n if (!context.item || !context.pullRequest || context.pullRequest.merged) return;\n if (context.board !== 'review') return;\n // A PR closed without merging is abandoned review work: clear the card off\n // the board instead of leaving it in Reviewing forever.\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:pull-request-closed`,\n board: 'review',\n stage: 'canceled',\n message: {\n text:\n `Pull request #${context.pullRequest.number} was closed without merging; ` +\n 'this Review card was moved to Canceled.',\n },\n } as const;\n}\n\nfunction reReviewRequestedPullRequest(context: FactoryGithubRuleContext) {\n // GitHub reviewer requests and Factory's exact mention command are both\n // explicit requests to enter the same Review lifecycle.\n const factoryReviewEntry = context.reviewRequest?.factoryReviewer || context.reviewCommand !== undefined;\n if ((context.item && context.board !== 'review') || !factoryReviewEntry) return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Trusted (write/admin) requesters only: creating or re-entering review checks\n // out and executes PR code, the same bar pullRequestOpened applies to auto-review.\n if (!trustedGithubActor(context)) return;\n if (context.actor.type === 'github' && context.actor.factoryAuthored) return;\n if (!context.item) {\n // On this path the actor is the *requester*, so the materialized\n // `authorTrusted` stamp records their trust (always true past the gate\n // above), not the PR author's: a trusted maintainer requesting a Factory\n // review vouches for the PR.\n return materializePullRequestIntake(context, {\n idempotencyKey: `${context.ingress.id}:pull-request-review-requested-intake`,\n autoStartCandidate: true,\n });\n }\n // Already in Reviewing: a review pass is pending or running; re-entering\n // would be a same-stage no-op anyway (stage rules only fire on change).\n if (context.item.stages.length === 1 && context.item.stages[0] === 'review') return;\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-requested`,\n board: 'review',\n stage: 'review',\n } as const;\n}\n\nfunction reReviewUpdatedPullRequest(context: FactoryGithubRuleContext) {\n if (!context.item || context.board !== 'review') return;\n if (!context.pullRequest || context.pullRequest.state !== 'open' || context.pullRequest.merged) return;\n // Intake has not started a review pass yet, so a push there is just more of\n // the code the first pass will read. A push to a card sitting in Reviewing is\n // different: it invalidates whatever that pass is reading, so re-enter the\n // stage to supersede it. `reviewPullRequest` cancels the stale run and picks\n // the right skill for the entry it sees.\n if (context.item.stages.some(stage => stage === 'intake')) return;\n const alreadyReviewing = context.item.stages.some(stage => stage === 'review');\n return {\n type: 'transition',\n idempotencyKey: `${context.ingress.id}:re-review-updated`,\n board: 'review',\n stage: 'review',\n // Re-entry is the point when the card is already Reviewing: the stage's\n // entry rule is what cancels the superseded pass and starts one on the code\n // that just landed.\n ...(alreadyReviewing ? { reenter: true } : {}),\n } as const;\n}\n\nexport const defaultGithubRules = Object.freeze({\n issueOpened: issueOpened,\n issueEdited: retriageGithubIssue,\n issueClosed: issueClosed,\n issueCommentCreated: retriageGithubIssue,\n issueCommentEdited: retriageGithubIssue,\n issueCommentDeleted: retriageGithubIssue,\n pullRequestOpened: pullRequestOpened,\n pullRequestUpdated: reReviewUpdatedPullRequest,\n pullRequestCommentCreated: addressPullRequestComment,\n pullRequestReviewRequested: reReviewRequestedPullRequest,\n pullRequestReviewSubmitted: addressReviewFeedback,\n pullRequestMerged: pullRequestMerged,\n pullRequestClosed: pullRequestClosed,\n} satisfies GithubEventRules);\n\nexport function resolveGithubRules(overrides?: GithubRuleOverrides): GithubEventRules {\n if (overrides !== undefined && (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides))) {\n throw new Error('GitHub rules must be an object.');\n }\n const rules: Record<string, FactoryRuleHandler<FactoryGithubRuleContext> | null> = { ...defaultGithubRules };\n for (const key of Reflect.ownKeys(overrides ?? {})) {\n if (typeof key !== 'string' || !Object.hasOwn(defaultGithubRules, key)) {\n throw new Error(`Unknown GitHub rule event: ${String(key)}.`);\n }\n const handler = overrides?.[key as FactoryGithubEventName];\n if (handler !== undefined && handler !== null && typeof handler !== 'function') {\n throw new Error(`GitHub rule ${key} must be a function, null, or undefined.`);\n }\n if (handler !== undefined) rules[key] = handler;\n }\n return Object.freeze(rules) as GithubEventRules;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"issue-reconciler.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/issue-reconciler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAUrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtH,MAAM,WAAW,2BAA2B;IAC1C,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5E;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,YAAY,EAAE,mBAAmB,EAAE,KAAK,OAAO,CAAC,2BAA2B,CAAC,CAAC;AAElH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,kBAAkB,GAC7B,qBAAqB,CA8HvB;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,kBAAkB,GAC7B,qBAAqB,GAAG,SAAS,CAKnC"}
1
+ {"version":3,"file":"issue-reconciler.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/issue-reconciler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAUrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtH,MAAM,WAAW,2BAA2B;IAC1C,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5E;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,YAAY,EAAE,mBAAmB,EAAE,KAAK,OAAO,CAAC,2BAA2B,CAAC,CAAC;AAElH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,kBAAkB,GAC7B,qBAAqB,CA6HvB;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,kBAAkB,GAC7B,qBAAqB,GAAG,SAAS,CAKnC"}
@@ -1,3 +1,5 @@
1
+ import { workItemPhaseSemantics } from "../../boards/semantics.js";
2
+ import "../../boards/index.js";
1
3
  import { GithubRules, githubRulesOptions, reconcilableIssueNumber, reconciledIssueClosedEvent, sameStrings, sweepTrustLookup } from "./rules.js";
2
4
  //#region src/integrations/github/issue-reconciler.ts
3
5
  function createGithubIssueReconciler(options, fetchIssue) {
@@ -37,8 +39,7 @@ function createGithubIssueReconciler(options, fetchIssue) {
37
39
  for (const item of items) {
38
40
  const issueNumber = reconcilableIssueNumber(item, repository);
39
41
  if (!issueNumber) continue;
40
- const stage = item.stages[0];
41
- if (stage === "done" || stage === "canceled") continue;
42
+ if (workItemPhaseSemantics(options.boards, item)?.kind === "terminal") continue;
42
43
  let list = itemsByNumber.get(issueNumber);
43
44
  if (!list) {
44
45
  list = [];
@@ -101,7 +102,7 @@ function createGithubIssueReconciler(options, fetchIssue) {
101
102
  };
102
103
  }
103
104
  function attachGithubIssueReconciler(github, context, fetchIssue) {
104
- if (!context.rules) return void 0;
105
+ if (!context.runtime) return void 0;
105
106
  const options = githubRulesOptions(github, context);
106
107
  if (!options) return void 0;
107
108
  return createGithubIssueReconciler(options, fetchIssue);
@@ -1 +1 @@
1
- {"version":3,"file":"issue-reconciler.js","names":[],"sources":["../../../src/integrations/github/issue-reconciler.ts"],"sourcesContent":["import type { IntegrationContext } from '../base.js';\nimport {\n githubRulesOptions,\n reconcilableIssueNumber,\n reconciledIssueClosedEvent,\n RECONCILE_ERROR_SAMPLE_LIMIT,\n sameStrings,\n sweepTrustLookup,\n GithubRules,\n} from './rules.js';\nimport type { GithubIssueFetcher, GithubRulesIntegration, GithubRulesOptions, ReconcileRepository } from './rules.js';\n\nexport interface GithubIssueReconcileSummary {\n /** Repositories included in the sweep. */\n repositories: number;\n /** Issue cards whose live state was fetched. */\n checked: number;\n /** Open issues with metadata patches. */\n updated: number;\n /** Closed issues replayed through the rules ingress. */\n closed: number;\n /** Errors encountered during the sweep. */\n failed: number;\n /** Error samples with context. */\n errors: Array<{ repository: string; issueNumber?: number; error: string }>;\n}\n\nexport type GithubIssueReconciler = (repositories: ReconcileRepository[]) => Promise<GithubIssueReconcileSummary>;\n\nexport function createGithubIssueReconciler(\n options: GithubRulesOptions,\n fetchIssue: GithubIssueFetcher,\n): GithubIssueReconciler {\n const rules = new GithubRules(options);\n return async repositories => {\n const summary: GithubIssueReconcileSummary = {\n repositories: 0,\n checked: 0,\n updated: 0,\n closed: 0,\n failed: 0,\n errors: [],\n };\n const recordFailure = (repository: ReconcileRepository, error: unknown, issueNumber?: number) => {\n summary.failed += 1;\n if (summary.errors.length < RECONCILE_ERROR_SAMPLE_LIMIT) {\n summary.errors.push({\n repository: repository.fullName,\n ...(issueNumber === undefined ? {} : { issueNumber }),\n error: error instanceof Error ? error.message : String(error),\n });\n }\n };\n\n for (const repository of repositories) {\n summary.repositories += 1;\n const authorTrust = sweepTrustLookup(options.github, repository);\n\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\n // Collect issue cards: number -> items (skip terminal stages)\n const itemsByNumber = new Map<number, import('../../storage/domains/work-items/base.js').WorkItemRow[]>();\n\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 issueNumber = reconcilableIssueNumber(item, repository);\n if (!issueNumber) continue;\n const stage = item.stages[0];\n if (stage === 'done' || stage === 'canceled') continue; // terminal, skip\n let list = itemsByNumber.get(issueNumber);\n if (!list) {\n list = [];\n itemsByNumber.set(issueNumber, list);\n }\n list.push(item);\n }\n }\n\n // Fetch and reconcile each unique issue\n for (const [issueNumber, items] of itemsByNumber) {\n try {\n const state = await fetchIssue({\n installationId: repository.installationId,\n repository: repository.fullName,\n number: issueNumber,\n });\n if (!state) continue; // missing or PR-backed\n summary.checked += 1;\n\n // Close detection: replay through rules ingress\n if (state.state === 'closed') {\n await rules.ingest(reconciledIssueClosedEvent(repository, issueNumber, state));\n summary.closed += 1;\n continue; // card transitions, skip metadata patch\n }\n\n // Metadata sync for open issues\n const desiredMetadata = {\n githubRepositoryId: repository.id,\n githubIssueNumber: issueNumber,\n state: 'open' as const,\n ...(state.author === undefined ? {} : { author: state.author }),\n ...(state.assignees === undefined ? {} : { assignees: state.assignees }),\n ...(state.labels === undefined ? {} : { labels: state.labels }),\n };\n // Re-stamped on every sweep so revoked write access reads untrusted\n // within one cycle; a failed lookup keeps the last stamp and retries.\n let authorTrusted: boolean | undefined;\n if (state.author !== undefined) {\n try {\n authorTrusted = await authorTrust(state.author);\n } catch (error) {\n recordFailure(repository, error, issueNumber);\n }\n }\n\n for (const item of items) {\n const current = item.metadata ?? {};\n const trustStale = authorTrusted !== undefined && current.authorTrusted !== authorTrusted;\n const metadataChanged =\n trustStale ||\n current.githubRepositoryId !== desiredMetadata.githubRepositoryId ||\n current.githubIssueNumber !== desiredMetadata.githubIssueNumber ||\n current.state !== desiredMetadata.state ||\n (state.author !== undefined && current.author !== state.author) ||\n (state.assignees !== undefined && !sameStrings(current.assignees, state.assignees)) ||\n (state.labels !== undefined && !sameStrings(current.labels, state.labels));\n\n if (!metadataChanged) continue;\n\n await options.storage.update({\n orgId: item.orgId,\n id: item.id,\n userId: 'factory-rule-dispatcher',\n patch: { metadata: { ...current, ...desiredMetadata, ...(trustStale ? { authorTrusted } : {}) } },\n });\n summary.updated += 1;\n }\n } catch (error) {\n recordFailure(repository, error, issueNumber);\n }\n }\n } catch (error) {\n recordFailure(repository, error);\n }\n }\n\n return summary;\n };\n}\n\nexport function attachGithubIssueReconciler(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n fetchIssue: GithubIssueFetcher,\n): GithubIssueReconciler | undefined {\n if (!context.rules) return undefined;\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n return createGithubIssueReconciler(options, fetchIssue);\n}"],"mappings":";;AA6BA,SAAgB,4BACd,SACA,YACuB;CACvB,MAAM,QAAQ,IAAI,YAAY,OAAO;CACrC,OAAO,OAAM,iBAAgB;EAC3B,MAAM,UAAuC;GAC3C,cAAc;GACd,SAAS;GACT,SAAS;GACT,QAAQ;GACR,QAAQ;GACR,QAAQ,CAAC;EACX;EACA,MAAM,iBAAiB,YAAiC,OAAgB,gBAAyB;GAC/F,QAAQ,UAAU;GAClB,IAAI,QAAQ,OAAO,SAAA,GACjB,QAAQ,OAAO,KAAK;IAClB,YAAY,WAAW;IACvB,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;IACnD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAAC;EAEL;EAEA,KAAK,MAAM,cAAc,cAAc;GACrC,QAAQ,gBAAgB;GACxB,MAAM,cAAc,iBAAiB,QAAQ,QAAQ,UAAU;GAE/D,IAAI;IACF,MAAM,WAAW,MAAM,QAAQ,cAAc,oBAAoB,yBAAyB;KACxF,wBAAwB,OAAO,WAAW,cAAc;KACxD,sBAAsB,OAAO,WAAW,EAAE;IAC5C,CAAC;IACD,IAAI,SAAS,WAAW,GAAG;IAG3B,MAAM,gCAAgB,IAAI,IAA8E;IAExG,KAAK,MAAM,WAAW,UAAU;KAC9B,MAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK;MACvC,OAAO,QAAQ;MACf,kBAAkB,QAAQ;KAC5B,CAAC;KACD,KAAK,MAAM,QAAQ,OAAO;MACxB,MAAM,cAAc,wBAAwB,MAAM,UAAU;MAC5D,IAAI,CAAC,aAAa;MAClB,MAAM,QAAQ,KAAK,OAAO;MAC1B,IAAI,UAAU,UAAU,UAAU,YAAY;MAC9C,IAAI,OAAO,cAAc,IAAI,WAAW;MACxC,IAAI,CAAC,MAAM;OACT,OAAO,CAAC;OACR,cAAc,IAAI,aAAa,IAAI;MACrC;MACA,KAAK,KAAK,IAAI;KAChB;IACF;IAGA,KAAK,MAAM,CAAC,aAAa,UAAU,eACjC,IAAI;KACF,MAAM,QAAQ,MAAM,WAAW;MAC7B,gBAAgB,WAAW;MAC3B,YAAY,WAAW;MACvB,QAAQ;KACV,CAAC;KACD,IAAI,CAAC,OAAO;KACZ,QAAQ,WAAW;KAGnB,IAAI,MAAM,UAAU,UAAU;MAC5B,MAAM,MAAM,OAAO,2BAA2B,YAAY,aAAa,KAAK,CAAC;MAC7E,QAAQ,UAAU;MAClB;KACF;KAGA,MAAM,kBAAkB;MACtB,oBAAoB,WAAW;MAC/B,mBAAmB;MACnB,OAAO;MACP,GAAI,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO;MAC7D,GAAI,MAAM,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,MAAM,UAAU;MACtE,GAAI,MAAM,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,MAAM,OAAO;KAC/D;KAGA,IAAI;KACJ,IAAI,MAAM,WAAW,KAAA,GACnB,IAAI;MACF,gBAAgB,MAAM,YAAY,MAAM,MAAM;KAChD,SAAS,OAAO;MACd,cAAc,YAAY,OAAO,WAAW;KAC9C;KAGF,KAAK,MAAM,QAAQ,OAAO;MACxB,MAAM,UAAU,KAAK,YAAY,CAAC;MAClC,MAAM,aAAa,kBAAkB,KAAA,KAAa,QAAQ,kBAAkB;MAU5E,IAAI,EARF,cACA,QAAQ,uBAAuB,gBAAgB,sBAC/C,QAAQ,sBAAsB,gBAAgB,qBAC9C,QAAQ,UAAU,gBAAgB,SACjC,MAAM,WAAW,KAAA,KAAa,QAAQ,WAAW,MAAM,UACvD,MAAM,cAAc,KAAA,KAAa,CAAC,YAAY,QAAQ,WAAW,MAAM,SAAS,KAChF,MAAM,WAAW,KAAA,KAAa,CAAC,YAAY,QAAQ,QAAQ,MAAM,MAAM,IAEpD;MAEtB,MAAM,QAAQ,QAAQ,OAAO;OAC3B,OAAO,KAAK;OACZ,IAAI,KAAK;OACT,QAAQ;OACR,OAAO,EAAE,UAAU;QAAE,GAAG;QAAS,GAAG;QAAiB,GAAI,aAAa,EAAE,cAAc,IAAI,CAAC;OAAG,EAAE;MAClG,CAAC;MACD,QAAQ,WAAW;KACrB;IACF,SAAS,OAAO;KACd,cAAc,YAAY,OAAO,WAAW;IAC9C;GAEJ,SAAS,OAAO;IACd,cAAc,YAAY,KAAK;GACjC;EACF;EAEA,OAAO;CACT;AACF;AAEA,SAAgB,4BACd,QACA,SACA,YACmC;CACnC,IAAI,CAAC,QAAQ,OAAO,OAAO,KAAA;CAC3B,MAAM,UAAU,mBAAmB,QAAQ,OAAO;CAClD,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,OAAO,4BAA4B,SAAS,UAAU;AACxD"}
1
+ {"version":3,"file":"issue-reconciler.js","names":[],"sources":["../../../src/integrations/github/issue-reconciler.ts"],"sourcesContent":["import { workItemPhaseSemantics } from '../../boards/index.js';\nimport type { IntegrationContext } from '../base.js';\nimport {\n githubRulesOptions,\n reconcilableIssueNumber,\n reconciledIssueClosedEvent,\n RECONCILE_ERROR_SAMPLE_LIMIT,\n sameStrings,\n sweepTrustLookup,\n GithubRules,\n} from './rules.js';\nimport type { GithubIssueFetcher, GithubRulesIntegration, GithubRulesOptions, ReconcileRepository } from './rules.js';\n\nexport interface GithubIssueReconcileSummary {\n /** Repositories included in the sweep. */\n repositories: number;\n /** Issue cards whose live state was fetched. */\n checked: number;\n /** Open issues with metadata patches. */\n updated: number;\n /** Closed issues replayed through the rules ingress. */\n closed: number;\n /** Errors encountered during the sweep. */\n failed: number;\n /** Error samples with context. */\n errors: Array<{ repository: string; issueNumber?: number; error: string }>;\n}\n\nexport type GithubIssueReconciler = (repositories: ReconcileRepository[]) => Promise<GithubIssueReconcileSummary>;\n\nexport function createGithubIssueReconciler(\n options: GithubRulesOptions,\n fetchIssue: GithubIssueFetcher,\n): GithubIssueReconciler {\n const rules = new GithubRules(options);\n return async repositories => {\n const summary: GithubIssueReconcileSummary = {\n repositories: 0,\n checked: 0,\n updated: 0,\n closed: 0,\n failed: 0,\n errors: [],\n };\n const recordFailure = (repository: ReconcileRepository, error: unknown, issueNumber?: number) => {\n summary.failed += 1;\n if (summary.errors.length < RECONCILE_ERROR_SAMPLE_LIMIT) {\n summary.errors.push({\n repository: repository.fullName,\n ...(issueNumber === undefined ? {} : { issueNumber }),\n error: error instanceof Error ? error.message : String(error),\n });\n }\n };\n\n for (const repository of repositories) {\n summary.repositories += 1;\n const authorTrust = sweepTrustLookup(options.github, repository);\n\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\n // Collect issue cards: number -> items (skip terminal stages)\n const itemsByNumber = new Map<number, import('../../storage/domains/work-items/base.js').WorkItemRow[]>();\n\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 issueNumber = reconcilableIssueNumber(item, repository);\n if (!issueNumber) continue;\n if (workItemPhaseSemantics(options.boards, item)?.kind === 'terminal') continue;\n let list = itemsByNumber.get(issueNumber);\n if (!list) {\n list = [];\n itemsByNumber.set(issueNumber, list);\n }\n list.push(item);\n }\n }\n\n // Fetch and reconcile each unique issue\n for (const [issueNumber, items] of itemsByNumber) {\n try {\n const state = await fetchIssue({\n installationId: repository.installationId,\n repository: repository.fullName,\n number: issueNumber,\n });\n if (!state) continue; // missing or PR-backed\n summary.checked += 1;\n\n // Close detection: replay through rules ingress\n if (state.state === 'closed') {\n await rules.ingest(reconciledIssueClosedEvent(repository, issueNumber, state));\n summary.closed += 1;\n continue; // card transitions, skip metadata patch\n }\n\n // Metadata sync for open issues\n const desiredMetadata = {\n githubRepositoryId: repository.id,\n githubIssueNumber: issueNumber,\n state: 'open' as const,\n ...(state.author === undefined ? {} : { author: state.author }),\n ...(state.assignees === undefined ? {} : { assignees: state.assignees }),\n ...(state.labels === undefined ? {} : { labels: state.labels }),\n };\n // Re-stamped on every sweep so revoked write access reads untrusted\n // within one cycle; a failed lookup keeps the last stamp and retries.\n let authorTrusted: boolean | undefined;\n if (state.author !== undefined) {\n try {\n authorTrusted = await authorTrust(state.author);\n } catch (error) {\n recordFailure(repository, error, issueNumber);\n }\n }\n\n for (const item of items) {\n const current = item.metadata ?? {};\n const trustStale = authorTrusted !== undefined && current.authorTrusted !== authorTrusted;\n const metadataChanged =\n trustStale ||\n current.githubRepositoryId !== desiredMetadata.githubRepositoryId ||\n current.githubIssueNumber !== desiredMetadata.githubIssueNumber ||\n current.state !== desiredMetadata.state ||\n (state.author !== undefined && current.author !== state.author) ||\n (state.assignees !== undefined && !sameStrings(current.assignees, state.assignees)) ||\n (state.labels !== undefined && !sameStrings(current.labels, state.labels));\n\n if (!metadataChanged) continue;\n\n await options.storage.update({\n orgId: item.orgId,\n id: item.id,\n userId: 'factory-rule-dispatcher',\n patch: { metadata: { ...current, ...desiredMetadata, ...(trustStale ? { authorTrusted } : {}) } },\n });\n summary.updated += 1;\n }\n } catch (error) {\n recordFailure(repository, error, issueNumber);\n }\n }\n } catch (error) {\n recordFailure(repository, error);\n }\n }\n\n return summary;\n };\n}\n\nexport function attachGithubIssueReconciler(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n fetchIssue: GithubIssueFetcher,\n): GithubIssueReconciler | undefined {\n if (!context.runtime) return undefined;\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n return createGithubIssueReconciler(options, fetchIssue);\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import type { FactoryRules } from '../../rules/types.js';
1
+ import type { BoardRegistry } from '../../boards/index.js';
2
+ import type { IntakeStorage } from '../../storage/domains/intake/base.js';
2
3
  import type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';
3
4
  import type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';
4
5
  import type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';
@@ -33,7 +34,10 @@ export interface GithubRulesOptions {
33
34
  integrationStorage: IntegrationStorageHandle;
34
35
  projects: FactoryProjectsStorage;
35
36
  storage: WorkItemsStorage;
36
- rules: Pick<FactoryRules, 'version'>;
37
+ configVersion: string;
38
+ boards: BoardRegistry;
39
+ /** Label routes decide which installed board a labelled issue lands on. Absent means Work. */
40
+ intake?: Pick<IntakeStorage, 'listLabelRoutes'>;
37
41
  }
38
42
  export declare class GithubRules {
39
43
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/rules.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/rules.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,10 @@
1
- import { isTerminalFactoryRuleStage } from "../../rules/types.js";
1
+ import { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from "../../rules/validation.js";
2
+ import { boardForWorkItem, workItemPhaseSemantics } from "../../boards/semantics.js";
3
+ import "../../boards/index.js";
2
4
  import { changeRequestTargetKey } from "./subscriptions.js";
3
- import { validateFactoryRuleDecisions } from "../../rules/validation.js";
4
- import { FACTORY_PULL_REQUEST_RECONCILIATION_KEY } from "../../storage/domains/work-items/base.js";
5
+ import { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, WorkItemUpdateConflictError } from "../../storage/domains/work-items/base.js";
6
+ import { cardLabels, moveCardToBoard } from "../../boards/relocate.js";
7
+ import { resolveIntakeLabelRoute } from "../../storage/domains/intake/base.js";
5
8
  //#region src/integrations/github/rules.ts
6
9
  const TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
7
10
  const RULE_TIMEOUT_MS = 5e3;
@@ -35,6 +38,9 @@ function actorLogins(value) {
35
38
  return login ? [login] : [];
36
39
  });
37
40
  }
41
+ function sameLabels(a, b) {
42
+ return a.length === b.length && a.every((label, index) => label === b[index]);
43
+ }
38
44
  function labelNames(value) {
39
45
  if (!Array.isArray(value)) return [];
40
46
  return value.flatMap((label) => {
@@ -150,6 +156,12 @@ function pullRequestProvenance(data, factoryProjectId) {
150
156
  workItemId: data.workItemId
151
157
  };
152
158
  }
159
+ /** Identity under which label-driven relocations are recorded. */
160
+ const LABEL_ROUTE_USER_ID = "factory-rule-dispatcher";
161
+ function issueLabelChange(parsed) {
162
+ const action = string(parsed.payload.action);
163
+ return parsed.event === "issues" && (action === "labeled" || action === "unlabeled");
164
+ }
153
165
  var GithubRules = class {
154
166
  options;
155
167
  constructor(options) {
@@ -174,21 +186,84 @@ var GithubRules = class {
174
186
  const slug = this.options.github.slug?.trim();
175
187
  return slug ? `${slug.toLowerCase()}[bot]` : void 0;
176
188
  }
189
+ /**
190
+ * Board an issue's labels select under the project's label routes, when that board is installed.
191
+ * Undefined leaves built-in routing (Work) in charge.
192
+ */
193
+ async #labelRouteTarget(orgId, factoryProjectId, labels) {
194
+ if (!this.options.intake || labels.length === 0) return void 0;
195
+ const route = resolveIntakeLabelRoute(await this.options.intake.listLabelRoutes({
196
+ orgId,
197
+ factoryProjectId,
198
+ integrationId: "github"
199
+ }), labels);
200
+ const board = route ? this.options.boards.get(route.board) : void 0;
201
+ return board ? {
202
+ board: board.id,
203
+ initialPhase: board.initialPhase
204
+ } : void 0;
205
+ }
206
+ /**
207
+ * `labeled` / `unlabeled` are not rule events: the labels only decide which board the card belongs
208
+ * on, so the card is re-routed here and its label metadata refreshed. Cards a run owns, and
209
+ * finished cards, stay where they are.
210
+ */
211
+ async #relocateLabeledIssue(parsed, repositoryId, repositoryName, project) {
212
+ const issue = object(parsed.payload.issue);
213
+ const issueNumber = issue?.pull_request === void 0 ? number(issue?.number) : void 0;
214
+ if (!issueNumber) return { status: "ignored" };
215
+ const found = await this.#relatedItem(project.orgId, project.factoryProjectId, repositoryId, repositoryName, issueNumber, void 0, void 0, null);
216
+ if (!found || found.externalSource?.type !== "issue") return { status: "ignored" };
217
+ const labels = labelNames(issue?.labels);
218
+ let item = found;
219
+ let changed = false;
220
+ if (!sameLabels(cardLabels(item), labels)) {
221
+ let updated;
222
+ try {
223
+ updated = await this.options.storage.update({
224
+ orgId: item.orgId,
225
+ id: item.id,
226
+ userId: LABEL_ROUTE_USER_ID,
227
+ patch: { metadata: {
228
+ ...item.metadata ?? {},
229
+ labels
230
+ } },
231
+ expectedRevision: item.revision
232
+ });
233
+ } catch (error) {
234
+ if (!(error instanceof WorkItemUpdateConflictError)) throw error;
235
+ return { status: "ignored" };
236
+ }
237
+ if (!updated) return { status: "ignored" };
238
+ item = updated.item;
239
+ changed = true;
240
+ }
241
+ const target = await this.#labelRouteTarget(project.orgId, project.factoryProjectId, labels);
242
+ const outcome = await moveCardToBoard({
243
+ workItems: this.options.storage,
244
+ boardRegistry: this.options.boards,
245
+ userId: LABEL_ROUTE_USER_ID,
246
+ item,
247
+ targetBoard: target?.board ?? "work"
248
+ });
249
+ return { status: changed || outcome === "moved" ? "committed" : "ignored" };
250
+ }
177
251
  async ingest(parsed) {
178
252
  const event = eventName(parsed);
253
+ const labelChange = issueLabelChange(parsed);
179
254
  const repository = object(parsed.payload.repository);
180
255
  const installationId = number(object(parsed.payload.installation)?.id);
181
256
  const repositoryId = number(repository?.id);
182
257
  const repositoryName = string(repository?.full_name);
183
258
  const login = string(object(parsed.payload.sender)?.login);
184
- if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: "ignored" };
259
+ if (!event && !labelChange || !installationId || !repositoryId || !repositoryName || !login) return { status: "ignored" };
185
260
  const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({
186
261
  installationExternalId: String(installationId),
187
262
  repositoryExternalId: String(repositoryId)
188
263
  });
189
264
  if (projects.length === 0) return { status: "ignored" };
190
265
  const results = [];
191
- for (const project of projects) results.push(await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project));
266
+ for (const project of projects) results.push(event ? await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project) : await this.#relocateLabeledIssue(parsed, repositoryId, repositoryName, project));
192
267
  if (results.some((result) => result.status === "committed")) return { status: "committed" };
193
268
  if (results.some((result) => result.status === "replayed")) return { status: "replayed" };
194
269
  return results[0] ?? { status: "ignored" };
@@ -217,6 +292,7 @@ var GithubRules = class {
217
292
  const pullRequestFactoryAuthored = provenance !== null || this.#isFactoryLogin(pullRequestAuthor);
218
293
  const resolvedItem = await this.#relatedItem(project.orgId, project.factoryProjectId, repositoryId, repositoryName, issueNumber, pullRequestNumber, string(object(pullRequest?.head)?.ref), reReviewEvent ? null : provenance, senderIsResponder && !reviewEntryRequested);
219
294
  const relatedItem = reviewEntryRequested && resolvedItem?.externalSource?.type !== "pull-request" ? void 0 : resolvedItem;
295
+ const intake = issueNumber ? await this.#labelRouteTarget(project.orgId, project.factoryProjectId, labelNames(issue?.labels)) : void 0;
220
296
  const actor = await githubActor(this.options.github, {
221
297
  installationId,
222
298
  repository: repositoryName,
@@ -237,7 +313,7 @@ var GithubRules = class {
237
313
  },
238
314
  cause: `github.${event}`,
239
315
  causalChain: [],
240
- ruleSetVersion: this.options.rules.version,
316
+ configVersion: this.options.configVersion,
241
317
  ...item ? {
242
318
  item: {
243
319
  id: item.id,
@@ -250,9 +326,10 @@ var GithubRules = class {
250
326
  acceptedAt: item.acceptedAt,
251
327
  metadata: item.metadata
252
328
  },
253
- board: item.externalSource?.type === "pull-request" ? "review" : "work",
329
+ board: boardForWorkItem(item),
254
330
  itemRevision: item.revision
255
331
  } : {},
332
+ ...intake ? { intake } : {},
256
333
  event,
257
334
  deliveryId: parsed.deliveryId,
258
335
  factory: { createdAt: factoryProject.createdAt.toISOString() },
@@ -322,7 +399,10 @@ var GithubRules = class {
322
399
  code: decision.code,
323
400
  reason: decision.reason
324
401
  };
325
- else if (decision) decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
402
+ else if (decision) decisions = validateFactoryRuleDecisions([decision]).map((entry) => {
403
+ assertFactoryDecisionTarget(entry, this.options.boards, item ? boardForWorkItem(item) : void 0);
404
+ return { ...entry };
405
+ });
326
406
  } catch (error) {
327
407
  const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
328
408
  outcome = {
@@ -339,7 +419,7 @@ var GithubRules = class {
339
419
  identity: ingressIdentity,
340
420
  triggerType: `github.${event}`
341
421
  },
342
- ruleSetVersion: this.options.rules.version,
422
+ configVersion: this.options.configVersion,
343
423
  expectedRevision: item?.revision ?? null,
344
424
  actor: { ...actor },
345
425
  outcome,
@@ -568,7 +648,6 @@ function createGithubPullRequestReconciler(options, fetchPullRequest) {
568
648
  factoryProjectId: project.factoryProjectId
569
649
  });
570
650
  for (const item of items) {
571
- const stage = item.stages[0];
572
651
  const unansweredAuthor = authorAwaitingTrust(item, repository);
573
652
  if (unansweredAuthor) unanswered.push({
574
653
  item,
@@ -579,7 +658,7 @@ function createGithubPullRequestReconciler(options, fetchPullRequest) {
579
658
  const metadata = item.metadata ?? {};
580
659
  const reconciliation = metadata[FACTORY_PULL_REQUEST_RECONCILIATION_KEY];
581
660
  const reconciledOutcome = reconciledPullRequestOutcome(metadata);
582
- if ((stage === "done" || stage === "canceled") && reconciledOutcome !== void 0 && reconciliation === reconciledOutcome) continue;
661
+ if (workItemPhaseSemantics(options.boards, item)?.kind === "terminal" && reconciledOutcome !== void 0 && reconciliation === reconciledOutcome) continue;
583
662
  const cards = cardsByNumber.get(pullRequestNumber) ?? [];
584
663
  cards.push(item);
585
664
  cardsByNumber.set(pullRequestNumber, cards);
@@ -640,7 +719,7 @@ function createGithubPullRequestReconciler(options, fetchPullRequest) {
640
719
  if (state.state !== "closed") continue;
641
720
  const cleanupFailures = /* @__PURE__ */ new Set();
642
721
  for (const card of cards) {
643
- if (!isTerminalFactoryRuleStage(card.stages)) continue;
722
+ if (workItemPhaseSemantics(options.boards, card)?.kind !== "terminal") continue;
644
723
  try {
645
724
  await options.storage.supersedeDecisionsForWorkItem({
646
725
  orgId: card.orgId,
@@ -679,14 +758,16 @@ function createGithubPullRequestReconciler(options, fetchPullRequest) {
679
758
  };
680
759
  }
681
760
  function githubRulesOptions(github, context) {
682
- if (!context.rules) return void 0;
761
+ if (!context.runtime) return void 0;
683
762
  return {
684
763
  github,
685
764
  sourceControl: context.storage.sourceControl,
686
765
  integrationStorage: context.storage.generic,
687
766
  projects: context.storage.projects,
688
- storage: context.rules.workItems,
689
- rules: context.rules.config
767
+ storage: context.runtime.workItems,
768
+ configVersion: context.runtime.configVersion,
769
+ boards: context.runtime.boards,
770
+ intake: context.storage.intake
690
771
  };
691
772
  }
692
773
  function attachGithubRules(github, context) {