@mastra/factory 0.10.0-alpha.2 → 0.10.0-alpha.7

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 (65) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/factory.js +2 -2
  3. package/dist/integrations/github/rules.d.ts.map +1 -1
  4. package/dist/integrations/github/rules.js +193 -127
  5. package/dist/integrations/github/rules.js.map +1 -1
  6. package/dist/routes/attention.d.ts +17 -0
  7. package/dist/routes/attention.d.ts.map +1 -0
  8. package/dist/routes/attention.js +305 -0
  9. package/dist/routes/attention.js.map +1 -0
  10. package/dist/routes/surface.d.ts +2 -3
  11. package/dist/routes/surface.d.ts.map +1 -1
  12. package/dist/routes/surface.js +60 -43
  13. package/dist/routes/surface.js.map +1 -1
  14. package/dist/routes/work-items.d.ts.map +1 -1
  15. package/dist/routes/work-items.js +34 -13
  16. package/dist/routes/work-items.js.map +1 -1
  17. package/dist/rules/dispatch-errors.d.ts +13 -0
  18. package/dist/rules/dispatch-errors.d.ts.map +1 -0
  19. package/dist/rules/dispatch-errors.js +77 -0
  20. package/dist/rules/dispatch-errors.js.map +1 -0
  21. package/dist/rules/dispatcher.d.ts.map +1 -1
  22. package/dist/rules/dispatcher.js +60 -35
  23. package/dist/rules/dispatcher.js.map +1 -1
  24. package/dist/rules/processor.d.ts.map +1 -1
  25. package/dist/rules/processor.js +30 -14
  26. package/dist/rules/processor.js.map +1 -1
  27. package/dist/rules/terminal-cleanup.d.ts +1 -1
  28. package/dist/rules/terminal-cleanup.d.ts.map +1 -1
  29. package/dist/rules/terminal-cleanup.js +2 -2
  30. package/dist/rules/terminal-cleanup.js.map +1 -1
  31. package/dist/rules/transition-service.d.ts +2 -1
  32. package/dist/rules/transition-service.d.ts.map +1 -1
  33. package/dist/rules/transition-service.js +1 -1
  34. package/dist/rules/transition-service.js.map +1 -1
  35. package/dist/rules/types.d.ts +2 -0
  36. package/dist/rules/types.d.ts.map +1 -1
  37. package/dist/rules/types.js +9 -1
  38. package/dist/rules/types.js.map +1 -1
  39. package/dist/session/factory-session.d.ts +4 -0
  40. package/dist/session/factory-session.d.ts.map +1 -1
  41. package/dist/session/factory-session.js +10 -2
  42. package/dist/session/factory-session.js.map +1 -1
  43. package/dist/session/first-exec-capture.d.ts +16 -10
  44. package/dist/session/first-exec-capture.d.ts.map +1 -1
  45. package/dist/session/first-exec-capture.js +74 -14
  46. package/dist/session/first-exec-capture.js.map +1 -1
  47. package/dist/session/first-message-capture.d.ts +14 -10
  48. package/dist/session/first-message-capture.d.ts.map +1 -1
  49. package/dist/session/first-message-capture.js +17 -11
  50. package/dist/session/first-message-capture.js.map +1 -1
  51. package/dist/storage/domains/source-control/base.d.ts +3 -0
  52. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  53. package/dist/storage/domains/source-control/base.js +8 -2
  54. package/dist/storage/domains/source-control/base.js.map +1 -1
  55. package/dist/storage/domains/work-items/base.d.ts +101 -10
  56. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  57. package/dist/storage/domains/work-items/base.js +458 -35
  58. package/dist/storage/domains/work-items/base.js.map +1 -1
  59. package/dist/work-item-branch.d.ts +23 -0
  60. package/dist/work-item-branch.d.ts.map +1 -0
  61. package/dist/work-item-branch.js +39 -0
  62. package/dist/work-item-branch.js.map +1 -0
  63. package/factory-skills/factory-rereview/SKILL.md +2 -0
  64. package/factory-skills/factory-review/SKILL.md +2 -0
  65. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # @mastra/factory
2
2
 
3
+ ## 0.10.0-alpha.7
4
+
5
+ ### Patch Changes
6
+
7
+ - Factory pull request review reports now show their selected model and reasoning setting: ([#22238](https://github.com/mastra-ai/mastra/pull/22238))
8
+
9
+ ```text
10
+ Review runtime: openai/gpt-5.6-sol, reasoning setting: high.
11
+ ```
12
+
13
+ - Fixed retried Factory skill runs so they deliver a fresh kickoff after execution errors while preserving duplicate protection during lease recovery. ([#21926](https://github.com/mastra-ai/mastra/pull/21926))
14
+
15
+ - Fixed session timing measurements that started too early or missed workspace tool activity. ([#22213](https://github.com/mastra-ai/mastra/pull/22213))
16
+
17
+ **First interaction time**
18
+ Starts on the first user or assistant message. Signal-only messages (skill loads, phase markers, memory reminders) and sessions that fail before a message no longer affect this metric.
19
+
20
+ **First meaningful tool time**
21
+ Starts when the first workspace tool completes successfully. File operations and workspace searches count even when no shell command runs. Approval-denied and abort-while-parked tool completions are excluded because the tool never actually ran.
22
+
23
+ - Updated dependencies [[`ae8790c`](https://github.com/mastra-ai/mastra/commit/ae8790c4bfaa088d2ab279d1dcc06f326b9fd109), [`04a815f`](https://github.com/mastra-ai/mastra/commit/04a815fc8971d29e97fcdcc5008a1eb472fc00ff), [`04a815f`](https://github.com/mastra-ai/mastra/commit/04a815fc8971d29e97fcdcc5008a1eb472fc00ff), [`cced745`](https://github.com/mastra-ai/mastra/commit/cced745a056ec2225c5bc702e32d848847aa8b65)]:
24
+ - @mastra/core@1.62.0-alpha.7
25
+ - @mastra/code-sdk@1.5.0-alpha.7
26
+
27
+ ## 0.10.0-alpha.6
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies [[`c8e4cea`](https://github.com/mastra-ai/mastra/commit/c8e4ceac9a390d78c8327dff3cdb2861dd71957f), [`ed01e9a`](https://github.com/mastra-ai/mastra/commit/ed01e9a807514a904374bf687a7b8f18750f6f78), [`4e9a228`](https://github.com/mastra-ai/mastra/commit/4e9a2283d5fd6ed1b70a2751eb3dc2cbf82ada20), [`63041eb`](https://github.com/mastra-ai/mastra/commit/63041eb4c50b520a0a80e03d4cd6ea99f67715a0)]:
32
+ - @mastra/core@1.62.0-alpha.6
33
+ - @mastra/code-sdk@1.5.0-alpha.6
34
+
35
+ ## 0.10.0-alpha.5
36
+
37
+ ### Patch Changes
38
+
39
+ - Fixed automated runs for manually created board cards. Moving a manual card into Planning or Building no longer fails with 'Factory skill invocation requires a supported issue or pull request identifier'. Manual cards now start on a stable `factory/item-<id>` branch, even without a provider identity. ([#22114](https://github.com/mastra-ai/mastra/pull/22114))
40
+
41
+ - Updated dependencies [[`65edab1`](https://github.com/mastra-ai/mastra/commit/65edab1c233d17b8f163bad12fca410d0e6f16b1), [`ab20a38`](https://github.com/mastra-ai/mastra/commit/ab20a38d0275f8d85e0f3833bd87ef487bcc609f), [`dbbfeb8`](https://github.com/mastra-ai/mastra/commit/dbbfeb85ec949dc9ebc0755e1ad262e4f5eba8db), [`3cc9d00`](https://github.com/mastra-ai/mastra/commit/3cc9d00b2b4333e0377a5e9df5eff92c17ce7630), [`733a537`](https://github.com/mastra-ai/mastra/commit/733a537489a858b5880b2e98809334fba895a221), [`9207dfa`](https://github.com/mastra-ai/mastra/commit/9207dfab8062e5fc68b751684797ff86fe0b4e70), [`12c61d2`](https://github.com/mastra-ai/mastra/commit/12c61d280c8cb208bc3c8dbcbe5dcc60cf9d1cd0), [`9a12ef3`](https://github.com/mastra-ai/mastra/commit/9a12ef3fccf3f4186db0f294f4ee1f02cf4d8db2)]:
42
+ - @mastra/core@1.62.0-alpha.5
43
+ - @mastra/code-sdk@1.5.0-alpha.5
44
+
45
+ ## 0.10.0-alpha.4
46
+
47
+ ### Patch Changes
48
+
49
+ - Updated dependencies [[`79f04a7`](https://github.com/mastra-ai/mastra/commit/79f04a7f6c6829da541139f638f2f1d267916e08), [`fd4d5fe`](https://github.com/mastra-ai/mastra/commit/fd4d5fe4f943699b85db5e74404f190d5a6b8c2a), [`f591643`](https://github.com/mastra-ai/mastra/commit/f591643becdf0be9bddce6ba1748e64bc30d77f1), [`b1ad324`](https://github.com/mastra-ai/mastra/commit/b1ad324d657f3544b0701332aef7eb10e9a36258), [`61c566d`](https://github.com/mastra-ai/mastra/commit/61c566dd2f2cde2b23ed8f139924e530d4202214)]:
50
+ - @mastra/core@1.62.0-alpha.4
51
+ - @mastra/code-sdk@1.5.0-alpha.4
52
+
53
+ ## 0.10.0-alpha.3
54
+
55
+ ### Minor Changes
56
+
57
+ - Added a durable Factory action center for unresolved automation failures and proposed work waiting for approval. Per-user read/archive receipts survive reloads, while retries and canonical reconciliation resolve failures for every project member. ([#22021](https://github.com/mastra-ai/mastra/pull/22021))
58
+
59
+ Historical decision state is repaired on startup: accepted transitions become `succeeded`, obsolete terminal work and proposals become `superseded`, and active unresolved failures remain `failed`. Retry is offered only when the persisted failure code allows it.
60
+
61
+ **Before**
62
+
63
+ ```ts
64
+ // Failed automation and proposed runs were visible only on their board cards.
65
+ ```
66
+
67
+ **After**
68
+
69
+ ```ts
70
+ const attention = await fetch(`/web/factory/projects/${factoryId}/attention`).then(response => response.json());
71
+ // attention.items: per-user unresolved failures
72
+ // attention.approvalCount: project-wide proposed work
73
+ ```
74
+
75
+ ### Patch Changes
76
+
77
+ - Fixed merged pull requests only reaching one of the two Factory cards that track them. A merge now both moves the Review card to Done and asks the work item that opened the pull request to assess whether its work is finished, no matter which card the merge event resolved to. ([#22135](https://github.com/mastra-ai/mastra/pull/22135))
78
+
79
+ - Updated dependencies [[`2c85f42`](https://github.com/mastra-ai/mastra/commit/2c85f428e04ccd63ea31a7ec80b5b327afdad555), [`11bbeb9`](https://github.com/mastra-ai/mastra/commit/11bbeb9b108ef2264e05acefc6dafb9cbb342921), [`1a485f3`](https://github.com/mastra-ai/mastra/commit/1a485f3538f5ec64d58bd8b5e1e99de0c695c87b), [`0d37487`](https://github.com/mastra-ai/mastra/commit/0d37487d9f349388a3f1cef6a536cf9dcc4b6273), [`8661d7d`](https://github.com/mastra-ai/mastra/commit/8661d7d7179f0a024456aabdd8679bcecd09ac28), [`575e343`](https://github.com/mastra-ai/mastra/commit/575e343900451021d96110916497d334af7bc252), [`cacb839`](https://github.com/mastra-ai/mastra/commit/cacb8392d9e74189b56d857290b0615f98a2683d), [`b47b26e`](https://github.com/mastra-ai/mastra/commit/b47b26e6fe95cb8a3482be2c5e52de157fe59d0b), [`0d37487`](https://github.com/mastra-ai/mastra/commit/0d37487d9f349388a3f1cef6a536cf9dcc4b6273), [`c46eb09`](https://github.com/mastra-ai/mastra/commit/c46eb09ce4987509af57a0ac582c61241a6dd2f1), [`30ed33e`](https://github.com/mastra-ai/mastra/commit/30ed33ee14084a26019aba15fceadda6d6ddefaf), [`91ad69d`](https://github.com/mastra-ai/mastra/commit/91ad69d64994c89199b0c55399e64ed91c61df2f), [`8dc408d`](https://github.com/mastra-ai/mastra/commit/8dc408d34438f9e13297f792c11a5cfd6cf952e1), [`c92def1`](https://github.com/mastra-ai/mastra/commit/c92def10a13c822972c96f0a4ca6ffc1f4258aed), [`c5eaec5`](https://github.com/mastra-ai/mastra/commit/c5eaec5a860d80d0e3805e67db0414b87ac8cbed), [`e66b2ba`](https://github.com/mastra-ai/mastra/commit/e66b2ba100db63eaeab6e21e1ea34b113f2ec781)]:
80
+ - @mastra/core@1.62.0-alpha.3
81
+ - @mastra/code-sdk@1.5.0-alpha.3
82
+
3
83
  ## 0.10.0-alpha.2
4
84
 
5
85
  ### Patch Changes
package/dist/factory.js CHANGED
@@ -5,6 +5,7 @@ import { SandboxFleet } from "./sandbox/fleet.js";
5
5
  import { BaseCheckpointBuilder } from "./sandbox/base-checkpoint.js";
6
6
  import { createBaseCheckpointTriggers } from "./sandbox/base-checkpoint-triggers.js";
7
7
  import { assertFactoryRules } from "./rules/validation.js";
8
+ import { WorkItemsStorage } from "./storage/domains/work-items/base.js";
8
9
  import { PlatformGithubIntegration } from "./integrations/platform/github/integration.js";
9
10
  import { PlatformLinearIntegration } from "./integrations/platform/linear/integration.js";
10
11
  import { createCustomProvidersPrimer, registerCustomProvidersSource } from "./routes/custom-provider-source.js";
@@ -12,13 +13,13 @@ import { ProjectRoutes } from "./routes/projects.js";
12
13
  import { MemorySettingsStorage } from "./storage/domains/memory-settings/base.js";
13
14
  import { hydrateSessionMemorySettings } from "./session/memory-settings-hydration.js";
14
15
  import { FactoryTransitionService } from "./rules/transition-service.js";
16
+ import { SourceControlStorage } from "./storage/domains/source-control/base.js";
15
17
  import { hydrateSessionModelPack } from "./session/model-pack-hydration.js";
16
18
  import { observeSessionFilesystem } from "./session/filesystem-capture.js";
17
19
  import { ModelCredentialsStorage } from "./storage/domains/credentials/base.js";
18
20
  import { FactoryWorkspaceRegistry, createWorkspaceFactory } from "./workspace.js";
19
21
  import { createTenantCredentialPrimer, primeTenantCredentials, registerTenantCredentialResolver } from "./routes/tenant-credentials.js";
20
22
  import { QueueHealthStorage } from "./storage/domains/queue-health/base.js";
21
- import { WorkItemsStorage } from "./storage/domains/work-items/base.js";
22
23
  import { assembleFactoryApiRoutes, buildIntegrationContext } from "./routes/surface.js";
23
24
  import { builtInFactoryRules } from "./rules/defaults.js";
24
25
  import { FactoryDecisionDispatcher } from "./rules/dispatcher.js";
@@ -43,7 +44,6 @@ import { IntakeStorage } from "./storage/domains/intake/base.js";
43
44
  import { IntegrationStorage } from "./storage/domains/integrations/base.js";
44
45
  import { ModelPacksStorage } from "./storage/domains/model-packs/base.js";
45
46
  import { FactoryProjectsStorage } from "./storage/domains/projects/base.js";
46
- import { SourceControlStorage } from "./storage/domains/source-control/base.js";
47
47
  import { hasAuthInit, isUserProvider } from "@mastra/core/server";
48
48
  import { MastraAuthStudio } from "@mastra/auth-studio";
49
49
  import { prepareAgentControllerMount } from "@mastra/code-sdk";
@@ -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,6 +1,8 @@
1
1
  import { changeRequestTargetKey } from "./subscriptions.js";
2
2
  import { resolveFactoryGithubRule } from "../../rules/resolve.js";
3
+ import { isTerminalFactoryRuleStage } from "../../rules/types.js";
3
4
  import { validateFactoryRuleDecisions } from "../../rules/validation.js";
5
+ import { FACTORY_PULL_REQUEST_RECONCILIATION_KEY } from "../../storage/domains/work-items/base.js";
4
6
  //#region src/integrations/github/rules.ts
5
7
  const TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
6
8
  const RULE_TIMEOUT_MS = 5e3;
@@ -178,124 +180,148 @@ var GithubRules = class {
178
180
  factoryAuthored: !senderIsResponder && provenance !== null || this.#isFactoryLogin(login)
179
181
  });
180
182
  if (actor.type === "github" && actor.factoryAuthored && (event === "issueCommentCreated" || event === "issueCommentEdited") && string(issueComment?.body)?.includes(FACTORY_TRIAGE_COMMENT_MARKER)) return { status: "ignored" };
181
- const context = {
182
- tenant: {
183
+ const evaluate = async (item, ingressIdentity) => {
184
+ const context = {
185
+ tenant: {
186
+ orgId: project.orgId,
187
+ projectId: project.factoryProjectId
188
+ },
189
+ actor,
190
+ ingress: {
191
+ type: "github",
192
+ id: ingressIdentity
193
+ },
194
+ cause: `github.${event}`,
195
+ causalChain: [],
196
+ ruleSetVersion: this.options.rules.version,
197
+ ...item ? {
198
+ item: {
199
+ id: item.id,
200
+ source: workItemSource(item),
201
+ sourceKey: workItemSourceKey(item),
202
+ parentWorkItemId: item.parentWorkItemId,
203
+ title: item.title,
204
+ url: item.externalSource?.url ?? null,
205
+ stages: item.stages,
206
+ metadata: item.metadata
207
+ },
208
+ board: item.externalSource?.type === "pull-request" ? "review" : "work",
209
+ itemRevision: item.revision
210
+ } : {},
211
+ event,
212
+ deliveryId: parsed.deliveryId,
213
+ factory: { createdAt: factoryProject.createdAt.toISOString() },
214
+ repository: {
215
+ id: repositoryId,
216
+ fullName: repositoryName
217
+ },
218
+ ...issueNumber && string(issue?.title) && string(issue?.html_url) ? { issue: {
219
+ number: issueNumber,
220
+ title: string(issue?.title),
221
+ url: string(issue?.html_url),
222
+ ...string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {},
223
+ ...string(issue?.updated_at) ? { updatedAt: string(issue?.updated_at) } : {},
224
+ assignees: actorLogins(issue?.assignees),
225
+ labels: labelNames(issue?.labels),
226
+ ...string(issue?.state) === "closed" || string(issue?.state) === "open" ? { state: string(issue?.state) } : {},
227
+ ...string(issue?.state_reason) ? { stateReason: string(issue?.state_reason) } : {}
228
+ } } : {},
229
+ ...event === "issueEdited" ? { issueChange: {
230
+ title: Boolean(object(changes?.title)),
231
+ body: Boolean(object(changes?.body))
232
+ } } : {},
233
+ ...number(issueComment?.id) ? { issueComment: {
234
+ id: number(issueComment?.id),
235
+ ...string(issueComment?.body) ? { body: string(issueComment?.body) } : {},
236
+ ...string(issueComment?.html_url) ? { url: string(issueComment?.html_url) } : {},
237
+ ...string(object(issueComment?.user)?.login) ? { author: string(object(issueComment?.user)?.login) } : {},
238
+ ...string(object(issueComment?.user)?.type) ? { authorType: string(object(issueComment?.user)?.type) } : {},
239
+ ...string(issueComment?.created_at) ? { createdAt: string(issueComment?.created_at) } : {},
240
+ ...string(issueComment?.updated_at) ? { updatedAt: string(issueComment?.updated_at) } : {}
241
+ } } : {},
242
+ ...pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url) ? { pullRequest: {
243
+ number: pullRequestNumber,
244
+ title: string(pullRequest?.title),
245
+ url: string(pullRequest?.html_url),
246
+ ...string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {},
247
+ state: string(pullRequest?.state) === "closed" ? "closed" : "open",
248
+ draft: boolean(pullRequest?.draft) ?? false,
249
+ merged: boolean(pullRequest?.merged) ?? false,
250
+ assignees: actorLogins(pullRequest?.assignees),
251
+ requestedReviewers: actorLogins(pullRequest?.requested_reviewers),
252
+ labels: labelNames(pullRequest?.labels),
253
+ headBranch: string(object(pullRequest?.head)?.ref) ?? "",
254
+ baseBranch: string(object(pullRequest?.base)?.ref) ?? ""
255
+ } } : {},
256
+ ...reviewRequested && requestedReviewer ? { reviewRequest: {
257
+ reviewer: requestedReviewer,
258
+ factoryReviewer: this.#isFactoryLogin(requestedReviewer)
259
+ } } : {},
260
+ ...object(parsed.payload.review) ? { review: {
261
+ id: number(object(parsed.payload.review)?.id) ?? 0,
262
+ state: string(object(parsed.payload.review)?.state) ?? "unknown",
263
+ url: string(object(parsed.payload.review)?.html_url) ?? ""
264
+ } } : {}
265
+ };
266
+ const rule = resolveFactoryGithubRule(this.options.rules, event);
267
+ let decision;
268
+ let decisions = [];
269
+ let outcome = { status: "accepted" };
270
+ try {
271
+ decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
272
+ if (decision?.type === "reject") outcome = {
273
+ status: "rejected",
274
+ code: decision.code,
275
+ reason: decision.reason
276
+ };
277
+ else if (decision) decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
278
+ } catch (error) {
279
+ const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
280
+ outcome = {
281
+ status: "rejected",
282
+ code: timedOut ? "timeout" : "rule_error",
283
+ reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory GitHub rule failed."
284
+ };
285
+ }
286
+ return { status: (await this.options.storage.commitRuleEvaluation({
183
287
  orgId: project.orgId,
184
- projectId: project.factoryProjectId
185
- },
186
- actor,
187
- ingress: {
188
- type: "github",
189
- id: `${installationId}:${parsed.deliveryId}`
190
- },
191
- cause: `github.${event}`,
192
- causalChain: [],
193
- ruleSetVersion: this.options.rules.version,
194
- ...relatedItem ? {
195
- item: {
196
- id: relatedItem.id,
197
- source: workItemSource(relatedItem),
198
- sourceKey: workItemSourceKey(relatedItem),
199
- parentWorkItemId: relatedItem.parentWorkItemId,
200
- title: relatedItem.title,
201
- url: relatedItem.externalSource?.url ?? null,
202
- stages: relatedItem.stages,
203
- metadata: relatedItem.metadata
288
+ factoryProjectId: project.factoryProjectId,
289
+ workItemId: item?.id ?? null,
290
+ ingress: {
291
+ identity: ingressIdentity,
292
+ triggerType: `github.${event}`
204
293
  },
205
- board: relatedItem.externalSource?.type === "pull-request" ? "review" : "work",
206
- itemRevision: relatedItem.revision
207
- } : {},
208
- event,
209
- deliveryId: parsed.deliveryId,
210
- factory: { createdAt: factoryProject.createdAt.toISOString() },
211
- repository: {
212
- id: repositoryId,
213
- fullName: repositoryName
214
- },
215
- ...issueNumber && string(issue?.title) && string(issue?.html_url) ? { issue: {
216
- number: issueNumber,
217
- title: string(issue?.title),
218
- url: string(issue?.html_url),
219
- ...string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {},
220
- ...string(issue?.updated_at) ? { updatedAt: string(issue?.updated_at) } : {},
221
- assignees: actorLogins(issue?.assignees),
222
- labels: labelNames(issue?.labels),
223
- ...string(issue?.state) === "closed" || string(issue?.state) === "open" ? { state: string(issue?.state) } : {},
224
- ...string(issue?.state_reason) ? { stateReason: string(issue?.state_reason) } : {}
225
- } } : {},
226
- ...event === "issueEdited" ? { issueChange: {
227
- title: Boolean(object(changes?.title)),
228
- body: Boolean(object(changes?.body))
229
- } } : {},
230
- ...number(issueComment?.id) ? { issueComment: {
231
- id: number(issueComment?.id),
232
- ...string(issueComment?.body) ? { body: string(issueComment?.body) } : {},
233
- ...string(issueComment?.html_url) ? { url: string(issueComment?.html_url) } : {},
234
- ...string(object(issueComment?.user)?.login) ? { author: string(object(issueComment?.user)?.login) } : {},
235
- ...string(object(issueComment?.user)?.type) ? { authorType: string(object(issueComment?.user)?.type) } : {},
236
- ...string(issueComment?.created_at) ? { createdAt: string(issueComment?.created_at) } : {},
237
- ...string(issueComment?.updated_at) ? { updatedAt: string(issueComment?.updated_at) } : {}
238
- } } : {},
239
- ...pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url) ? { pullRequest: {
240
- number: pullRequestNumber,
241
- title: string(pullRequest?.title),
242
- url: string(pullRequest?.html_url),
243
- ...string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {},
244
- state: string(pullRequest?.state) === "closed" ? "closed" : "open",
245
- draft: boolean(pullRequest?.draft) ?? false,
246
- merged: boolean(pullRequest?.merged) ?? false,
247
- assignees: actorLogins(pullRequest?.assignees),
248
- requestedReviewers: actorLogins(pullRequest?.requested_reviewers),
249
- labels: labelNames(pullRequest?.labels),
250
- headBranch: string(object(pullRequest?.head)?.ref) ?? "",
251
- baseBranch: string(object(pullRequest?.base)?.ref) ?? ""
252
- } } : {},
253
- ...reviewRequested && requestedReviewer ? { reviewRequest: {
254
- reviewer: requestedReviewer,
255
- factoryReviewer: this.#isFactoryLogin(requestedReviewer)
256
- } } : {},
257
- ...object(parsed.payload.review) ? { review: {
258
- id: number(object(parsed.payload.review)?.id) ?? 0,
259
- state: string(object(parsed.payload.review)?.state) ?? "unknown",
260
- url: string(object(parsed.payload.review)?.html_url) ?? ""
261
- } } : {}
294
+ ruleSetVersion: this.options.rules.version,
295
+ expectedRevision: item?.revision ?? null,
296
+ actor: { ...actor },
297
+ outcome,
298
+ decisions,
299
+ causalChain: [],
300
+ now: /* @__PURE__ */ new Date()
301
+ })).status };
262
302
  };
263
- const rule = resolveFactoryGithubRule(this.options.rules, event);
264
- let decision;
265
- let decisions = [];
266
- let outcome = { status: "accepted" };
267
- try {
268
- decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
269
- if (decision?.type === "reject") outcome = {
270
- status: "rejected",
271
- code: decision.code,
272
- reason: decision.reason
273
- };
274
- else if (decision) decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
275
- } catch (error) {
276
- const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
277
- outcome = {
278
- status: "rejected",
279
- code: timedOut ? "timeout" : "rule_error",
280
- reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory GitHub rule failed."
281
- };
282
- }
283
- return { status: (await this.options.storage.commitRuleEvaluation({
284
- orgId: project.orgId,
285
- factoryProjectId: project.factoryProjectId,
286
- workItemId: relatedItem?.id ?? null,
287
- ingress: {
288
- identity: `${installationId}:${parsed.deliveryId}`,
289
- triggerType: `github.${event}`
290
- },
291
- ruleSetVersion: this.options.rules.version,
292
- expectedRevision: relatedItem?.revision ?? null,
293
- actor: { ...actor },
294
- outcome,
295
- decisions,
296
- causalChain: [],
297
- now: /* @__PURE__ */ new Date()
298
- })).status };
303
+ const deliveryIdentity = `${installationId}:${parsed.deliveryId}`;
304
+ const primary = await evaluate(relatedItem, deliveryIdentity);
305
+ const linked = event === "pullRequestMerged" && relatedItem && pullRequestNumber ? await this.#linkedClosureItem(project.orgId, project.factoryProjectId, repositoryId, repositoryName, pullRequestNumber, relatedItem) : void 0;
306
+ if (!linked) return primary;
307
+ const secondary = await evaluate(linked, `${deliveryIdentity}:${linked.id}`);
308
+ for (const status of ["committed", "replayed"]) if (primary.status === status || secondary.status === status) return { status };
309
+ return primary;
310
+ }
311
+ /**
312
+ * The other card a closed pull request concerns, joined through the Review
313
+ * card's `parentWorkItemId` — the link `upsertLinkedWorkItem` records when the
314
+ * pull request is opened, and therefore an exact join that needs no branch
315
+ * heuristics. Returns nothing when the pull request has only one card, which
316
+ * is every pull request Factory did not open from a work item's session.
317
+ */
318
+ async #linkedClosureItem(orgId, projectId, repositoryId, repositoryFullName, pullRequestNumber, resolved) {
319
+ const items = await this.options.storage.list({
320
+ orgId,
321
+ factoryProjectId: projectId
322
+ });
323
+ const linked = resolved.externalSource?.type === "pull-request" ? items.find((item) => item.id === resolved.parentWorkItemId) : items.find((item) => item.parentWorkItemId === resolved.id && (item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber) || item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)) && cardBelongsToRepository(item, repositoryId, repositoryFullName));
324
+ return linked?.id === resolved.id ? void 0 : linked;
299
325
  }
300
326
  async #relatedItem(orgId, projectId, repositoryId, repositoryFullName, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance, preferAuthoringItem = false) {
301
327
  const items = await this.options.storage.list({
@@ -418,6 +444,22 @@ function reconciledClosedEvent(repository, pullRequestNumber, state) {
418
444
  }
419
445
  };
420
446
  }
447
+ function reconciledPullRequestMetadata(state, reconciliation) {
448
+ return {
449
+ state: state.state,
450
+ draft: state.draft,
451
+ merged: state.merged,
452
+ ...state.author ? { author: state.author } : {},
453
+ ...state.assignees ? { assignees: state.assignees } : {},
454
+ ...state.requestedReviewers ? { requestedReviewers: state.requestedReviewers } : {},
455
+ ...state.labels ? { labels: state.labels } : {},
456
+ [FACTORY_PULL_REQUEST_RECONCILIATION_KEY]: reconciliation === "settled" ? state.merged ? "merged" : "closed" : null
457
+ };
458
+ }
459
+ function reconciledPullRequestOutcome(metadata) {
460
+ if (metadata.state !== "closed" || typeof metadata.merged !== "boolean") return void 0;
461
+ return metadata.merged ? "merged" : "closed";
462
+ }
421
463
  /**
422
464
  * The webhook handler retires subscriptions itself; the sweep replays only the
423
465
  * rules ingress, so without this the thread's PR chip and the workspace row
@@ -479,8 +521,9 @@ function createGithubPullRequestReconciler(options, fetchPullRequest) {
479
521
  const pullRequestNumber = reconcilablePullRequestNumber(item, repository);
480
522
  if (!pullRequestNumber) continue;
481
523
  const metadata = item.metadata ?? {};
482
- const pullRequestSettled = metadata.state === "closed" && typeof metadata.draft === "boolean" && typeof metadata.merged === "boolean" && typeof metadata.author === "string" && Array.isArray(metadata.assignees) && Array.isArray(metadata.requestedReviewers) && Array.isArray(metadata.labels);
483
- if ((stage === "done" || stage === "canceled") && pullRequestSettled) continue;
524
+ const reconciliation = metadata[FACTORY_PULL_REQUEST_RECONCILIATION_KEY];
525
+ const reconciledOutcome = reconciledPullRequestOutcome(metadata);
526
+ if ((stage === "done" || stage === "canceled") && reconciledOutcome !== void 0 && reconciliation === reconciledOutcome) continue;
484
527
  const cards = cardsByNumber.get(pullRequestNumber) ?? [];
485
528
  cards.push(item);
486
529
  cardsByNumber.set(pullRequestNumber, cards);
@@ -499,35 +542,58 @@ function createGithubPullRequestReconciler(options, fetchPullRequest) {
499
542
  summary.checked += 1;
500
543
  if (!state) continue;
501
544
  for (const card of cards) {
545
+ if (state.state === "closed") continue;
502
546
  const metadata = card.metadata ?? {};
503
547
  const statusChanged = metadata.state !== state.state || metadata.draft !== state.draft || metadata.merged !== state.merged;
504
548
  const authorChanged = state.author !== void 0 && metadata.author !== state.author;
505
549
  const assigneesChanged = !sameStrings(metadata.assignees, state.assignees);
506
550
  const reviewersChanged = !sameStrings(metadata.requestedReviewers, state.requestedReviewers);
507
551
  const labelsChanged = !sameStrings(metadata.labels, state.labels);
508
- if (!statusChanged && !authorChanged && !assigneesChanged && !reviewersChanged && !labelsChanged) continue;
552
+ const metadataChanged = statusChanged || authorChanged || assigneesChanged || reviewersChanged || labelsChanged;
553
+ const reconciliation = metadata[FACTORY_PULL_REQUEST_RECONCILIATION_KEY];
554
+ if (!metadataChanged && reconciliation !== "merged" && reconciliation !== "closed") continue;
509
555
  try {
510
556
  await options.storage.update({
511
557
  orgId: card.orgId,
512
558
  id: card.id,
513
559
  userId: "factory-rule-dispatcher",
514
- patch: { metadata: {
515
- state: state.state,
516
- draft: state.draft,
517
- merged: state.merged,
518
- ...state.author ? { author: state.author } : {},
519
- ...state.assignees ? { assignees: state.assignees } : {},
520
- ...state.requestedReviewers ? { requestedReviewers: state.requestedReviewers } : {},
521
- ...state.labels ? { labels: state.labels } : {}
522
- } }
560
+ patch: { metadata: reconciledPullRequestMetadata(state, "clear") }
523
561
  });
524
562
  } catch (error) {
525
563
  recordFailure(repository, error, pullRequestNumber);
526
564
  }
527
565
  }
528
566
  if (state.state !== "closed") continue;
567
+ const cleanupFailures = /* @__PURE__ */ new Set();
568
+ for (const card of cards) {
569
+ if (!isTerminalFactoryRuleStage(card.stages)) continue;
570
+ try {
571
+ await options.storage.supersedeDecisionsForWorkItem({
572
+ orgId: card.orgId,
573
+ factoryProjectId: card.factoryProjectId,
574
+ workItemId: card.id,
575
+ supersededAt: /* @__PURE__ */ new Date()
576
+ });
577
+ } catch (error) {
578
+ recordFailure(repository, error, pullRequestNumber);
579
+ cleanupFailures.add(card.id);
580
+ }
581
+ }
529
582
  await rules.ingest(reconciledClosedEvent(repository, pullRequestNumber, state));
530
583
  await retireReconciledSubscriptions(options.integrationStorage, repository, pullRequestNumber, state.merged);
584
+ for (const card of cards) {
585
+ if (cleanupFailures.has(card.id)) continue;
586
+ try {
587
+ await options.storage.update({
588
+ orgId: card.orgId,
589
+ id: card.id,
590
+ userId: "factory-rule-dispatcher",
591
+ patch: { metadata: reconciledPullRequestMetadata(state, "settled") }
592
+ });
593
+ } catch (error) {
594
+ recordFailure(repository, error, pullRequestNumber);
595
+ }
596
+ }
531
597
  if (state.merged) summary.merged += 1;
532
598
  else summary.closed += 1;
533
599
  } catch (error) {