@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2

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 (174) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +401 -314
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/connection.js +1 -1
  7. package/dist/capabilities/intake.js +1 -1
  8. package/dist/capabilities/version-control.js +1 -1
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +441 -39455
  11. package/dist/factory.js.map +1 -1
  12. package/dist/index.js +2 -39466
  13. package/dist/integrations/base.js +1 -1
  14. package/dist/integrations/github/config.js +36 -22
  15. package/dist/integrations/github/config.js.map +1 -1
  16. package/dist/integrations/github/factory-session.js +33 -37
  17. package/dist/integrations/github/factory-session.js.map +1 -1
  18. package/dist/integrations/github/integration.js +1037 -3624
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-triage.js +71 -71
  21. package/dist/integrations/github/issue-triage.js.map +1 -1
  22. package/dist/integrations/github/pat.js +47 -36
  23. package/dist/integrations/github/pat.js.map +1 -1
  24. package/dist/integrations/github/provenance.js +72 -101
  25. package/dist/integrations/github/provenance.js.map +1 -1
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +1272 -1833
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/rules.js +203 -471
  30. package/dist/integrations/github/rules.js.map +1 -1
  31. package/dist/integrations/github/sandbox.d.ts +2 -0
  32. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  33. package/dist/integrations/github/sandbox.js +401 -326
  34. package/dist/integrations/github/sandbox.js.map +1 -1
  35. package/dist/integrations/github/session-subscriptions.js +165 -223
  36. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  37. package/dist/integrations/github/subscriptions.js +43 -51
  38. package/dist/integrations/github/subscriptions.js.map +1 -1
  39. package/dist/integrations/github/token-refresh.js +15 -18
  40. package/dist/integrations/github/token-refresh.js.map +1 -1
  41. package/dist/integrations/github/webhook.js +359 -343
  42. package/dist/integrations/github/webhook.js.map +1 -1
  43. package/dist/integrations/linear/agent-tools.js +77 -100
  44. package/dist/integrations/linear/agent-tools.js.map +1 -1
  45. package/dist/integrations/linear/integration.js +611 -1309
  46. package/dist/integrations/linear/integration.js.map +1 -1
  47. package/dist/integrations/linear/routes.js +227 -230
  48. package/dist/integrations/linear/routes.js.map +1 -1
  49. package/dist/integrations/linear/rules.js +126 -346
  50. package/dist/integrations/linear/rules.js.map +1 -1
  51. package/dist/integrations/linear/storage.js +1 -1
  52. package/dist/integrations/platform/api-client.js +131 -135
  53. package/dist/integrations/platform/api-client.js.map +1 -1
  54. package/dist/integrations/platform/github/event-worker.js +275 -573
  55. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  56. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  57. package/dist/integrations/platform/github/integration.js +845 -3942
  58. package/dist/integrations/platform/github/integration.js.map +1 -1
  59. package/dist/integrations/platform/linear/integration.js +457 -1309
  60. package/dist/integrations/platform/linear/integration.js.map +1 -1
  61. package/dist/integrations/workos/integration.js +80 -85
  62. package/dist/integrations/workos/integration.js.map +1 -1
  63. package/dist/routes/config.d.ts.map +1 -1
  64. package/dist/routes/config.js +933 -915
  65. package/dist/routes/config.js.map +1 -1
  66. package/dist/routes/custom-provider-source.js +110 -93
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.js +401 -349
  69. package/dist/routes/fs.js.map +1 -1
  70. package/dist/routes/intake.js +165 -165
  71. package/dist/routes/intake.js.map +1 -1
  72. package/dist/routes/oauth.js +447 -25166
  73. package/dist/routes/oauth.js.map +1 -1
  74. package/dist/routes/projects.js +437 -376
  75. package/dist/routes/projects.js.map +1 -1
  76. package/dist/routes/provider-credentials.js +76 -58
  77. package/dist/routes/provider-credentials.js.map +1 -1
  78. package/dist/routes/route.js +12 -8
  79. package/dist/routes/route.js.map +1 -1
  80. package/dist/routes/skills.js +160 -173
  81. package/dist/routes/skills.js.map +1 -1
  82. package/dist/routes/surface.js +250 -28618
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js +141 -166
  85. package/dist/routes/tenant-credentials.js.map +1 -1
  86. package/dist/routes/work-items.js +654 -862
  87. package/dist/routes/work-items.js.map +1 -1
  88. package/dist/rules/binding-context.js +23 -35
  89. package/dist/rules/binding-context.js.map +1 -1
  90. package/dist/rules/defaults.js +200 -309
  91. package/dist/rules/defaults.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +438 -677
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/index.js +5 -599
  96. package/dist/rules/processor.js +344 -596
  97. package/dist/rules/processor.js.map +1 -1
  98. package/dist/rules/resolve.js +21 -18
  99. package/dist/rules/resolve.js.map +1 -1
  100. package/dist/rules/start-coordinator.js +168 -157
  101. package/dist/rules/start-coordinator.js.map +1 -1
  102. package/dist/rules/tools.js +50 -85
  103. package/dist/rules/tools.js.map +1 -1
  104. package/dist/rules/transition-service.js +183 -454
  105. package/dist/rules/transition-service.js.map +1 -1
  106. package/dist/rules/types.js +33 -29
  107. package/dist/rules/types.js.map +1 -1
  108. package/dist/rules/validation.js +266 -281
  109. package/dist/rules/validation.js.map +1 -1
  110. package/dist/sandbox/fleet.js +272 -250
  111. package/dist/sandbox/fleet.js.map +1 -1
  112. package/dist/sandbox/reattach.js +13 -6
  113. package/dist/sandbox/reattach.js.map +1 -1
  114. package/dist/server-error.js +12 -16
  115. package/dist/server-error.js.map +1 -1
  116. package/dist/skills/service.js +30 -35
  117. package/dist/skills/service.js.map +1 -1
  118. package/dist/spa-static.d.ts.map +1 -1
  119. package/dist/spa-static.js +91 -64
  120. package/dist/spa-static.js.map +1 -1
  121. package/dist/state-signing.js +73 -44
  122. package/dist/state-signing.js.map +1 -1
  123. package/dist/storage/domains/audit/agent-audit.js +60 -59
  124. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  125. package/dist/storage/domains/audit/base.js +169 -112
  126. package/dist/storage/domains/audit/base.js.map +1 -1
  127. package/dist/storage/domains/audit/domain.js +155 -153
  128. package/dist/storage/domains/audit/domain.js.map +1 -1
  129. package/dist/storage/domains/credentials/base.js +259 -243
  130. package/dist/storage/domains/credentials/base.js.map +1 -1
  131. package/dist/storage/domains/custom-providers/base.js +138 -143
  132. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  133. package/dist/storage/domains/intake/base.js +75 -57
  134. package/dist/storage/domains/intake/base.js.map +1 -1
  135. package/dist/storage/domains/integrations/base.js +310 -245
  136. package/dist/storage/domains/integrations/base.js.map +1 -1
  137. package/dist/storage/domains/memory-settings/base.js +117 -102
  138. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  139. package/dist/storage/domains/model-packs/base.js +89 -88
  140. package/dist/storage/domains/model-packs/base.js.map +1 -1
  141. package/dist/storage/domains/projects/base.js +97 -92
  142. package/dist/storage/domains/projects/base.js.map +1 -1
  143. package/dist/storage/domains/queue-health/base.js +111 -76
  144. package/dist/storage/domains/queue-health/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +9 -0
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +777 -702
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +322 -344
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  154. package/dist/storage/domains/work-items/base.js +1266 -1132
  155. package/dist/storage/domains/work-items/base.js.map +1 -1
  156. package/dist/storage/domains/work-items/metrics.js +202 -184
  157. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  158. package/dist/timing.d.ts +15 -0
  159. package/dist/timing.d.ts.map +1 -0
  160. package/dist/timing.js +34 -0
  161. package/dist/timing.js.map +1 -0
  162. package/dist/workspace.d.ts +2 -3
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +236 -438
  165. package/dist/workspace.js.map +1 -1
  166. package/factory-skills/factory-review/SKILL.md +9 -2
  167. package/package.json +9 -9
  168. package/dist/capabilities/connection.js.map +0 -1
  169. package/dist/capabilities/intake.js.map +0 -1
  170. package/dist/capabilities/version-control.js.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/integrations/base.js.map +0 -1
  173. package/dist/integrations/linear/storage.js.map +0 -1
  174. package/dist/rules/index.js.map +0 -1
@@ -1,629 +1,377 @@
1
- // src/rules/processor.ts
1
+ import { resolveFactoryToolRule } from "./resolve.js";
2
+ import { FACTORY_RULE_STAGES } from "./types.js";
3
+ import { normalizeFactoryRuleJsonValue, validateFactoryRuleDecisions } from "./validation.js";
4
+ import { getFactorySessionCoordinates } from "./binding-context.js";
2
5
  import { createHash } from "crypto";
3
-
4
- // src/auth.ts
5
- import { MastraAuthWorkos } from "@mastra/auth-workos";
6
- import {
7
- registerApiRoute,
8
- isAuthHttpHandler,
9
- isCredentialsProvider,
10
- isOrganizationsProvider,
11
- isSessionProvider,
12
- isSSOProvider
13
- } from "@mastra/core/server";
14
-
15
- // src/rules/binding-context.ts
16
- function getFactorySessionCoordinates(requestContext) {
17
- if (!requestContext || typeof requestContext.get !== "function") return null;
18
- const context = requestContext.get("controller");
19
- const factoryProjectId = context?.getState().factoryProjectId;
20
- if (!context?.threadId || !context.resourceId || !factoryProjectId) return null;
21
- return {
22
- factoryProjectId,
23
- threadId: context.threadId,
24
- resourceId: context.resourceId,
25
- sessionId: context.resourceId
26
- };
27
- }
28
-
29
- // src/rules/resolve.ts
30
- function resolveFactoryToolRule(rules, toolName) {
31
- return rules.tools[toolName]?.onResult;
32
- }
33
-
34
- // src/rules/types.ts
35
- var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
36
- var FACTORY_RULE_BOARDS = ["work", "review"];
37
-
38
- // src/rules/validation.ts
39
- var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
40
- var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
41
- var MAX_REASON_LENGTH = 512;
42
- var MAX_TITLE_LENGTH = 512;
43
- var MAX_MESSAGE_LENGTH = 8192;
44
- var MAX_ARGUMENTS_LENGTH = 4096;
45
- var MAX_ROLE_LENGTH = 32;
46
- var MAX_SKILL_NAME_LENGTH = 128;
47
- var MAX_SOURCE_KEY_LENGTH = 256;
48
- var MAX_URL_LENGTH = 2048;
49
- var MAX_METADATA_JSON_LENGTH = 16384;
50
- var MAX_JSON_DEPTH = 8;
51
- var MAX_JSON_COLLECTION_SIZE = 100;
52
- var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
53
- var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
54
- var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
55
- var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
56
- var REJECTION_CODES = [
57
- "forbidden",
58
- "invalid_transition",
59
- "missing_binding",
60
- "stale",
61
- "timeout",
62
- "rule_error",
63
- "causal_depth_exceeded",
64
- "repeated_transition"
65
- ];
66
- var FactoryRuleValidationError = class extends Error {
67
- code = "invalid_factory_rule";
68
- constructor(message) {
69
- super(message);
70
- this.name = "FactoryRuleValidationError";
71
- }
72
- };
73
- function isPlainObject(value) {
74
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
75
- const prototype = Object.getPrototypeOf(value);
76
- return prototype === Object.prototype || prototype === null;
77
- }
78
- function assertExactKeys(value, keys, label) {
79
- const allowed = new Set(keys);
80
- if (Object.keys(value).some((key) => !allowed.has(key))) {
81
- throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
82
- }
83
- }
84
- function boundedString(value, label, max, pattern) {
85
- if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
86
- const normalized = value.trim();
87
- if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
88
- throw new FactoryRuleValidationError(`${label} is invalid.`);
89
- }
90
- return normalized;
91
- }
92
- function optionalBoundedString(value, label, max) {
93
- if (value === void 0) return void 0;
94
- return boundedString(value, label, max);
95
- }
96
- function enumValue(value, allowed, label) {
97
- if (typeof value !== "string" || !allowed.includes(value)) {
98
- throw new FactoryRuleValidationError(`${label} is invalid.`);
99
- }
100
- return value;
101
- }
102
- function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
103
- if (value === null || typeof value === "boolean" || typeof value === "string") return value;
104
- if (typeof value === "number") {
105
- if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
106
- return value;
107
- }
108
- if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
109
- throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
110
- }
111
- if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
112
- seen.add(value);
113
- try {
114
- if (Array.isArray(value)) {
115
- if (value.length > MAX_JSON_COLLECTION_SIZE) {
116
- throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
117
- }
118
- return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
119
- }
120
- if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
121
- const entries = Object.entries(value);
122
- if (entries.length > MAX_JSON_COLLECTION_SIZE) {
123
- throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
124
- }
125
- const sanitized = {};
126
- for (const [key, entry] of entries) {
127
- const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
128
- sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
129
- }
130
- return sanitized;
131
- } finally {
132
- seen.delete(value);
133
- }
134
- }
135
- function sanitizeMetadata(value) {
136
- if (value === void 0) return void 0;
137
- const sanitized = normalizeFactoryRuleJsonValue(value);
138
- if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
139
- if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
140
- throw new FactoryRuleValidationError("Rule metadata is too large.");
141
- }
142
- return sanitized;
143
- }
144
- function commonCommitFields(value) {
145
- return {
146
- idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
147
- };
148
- }
149
- function validateFactoryRuleDecision(value, causalDepth = 0) {
150
- if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
151
- throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
152
- }
153
- if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
154
- const type = value.type;
155
- if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
156
- switch (type) {
157
- case "reject": {
158
- assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
159
- return {
160
- type,
161
- code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
162
- reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
163
- };
164
- }
165
- case "transition": {
166
- assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
167
- return {
168
- type,
169
- ...commonCommitFields(value),
170
- board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
171
- stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
172
- };
173
- }
174
- case "upsertLinkedWorkItem": {
175
- assertExactKeys(
176
- value,
177
- ["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
178
- "Factory linked work item decision"
179
- );
180
- const url = value.url;
181
- if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
182
- throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
183
- }
184
- const metadata = sanitizeMetadata(value.metadata);
185
- return {
186
- type,
187
- ...commonCommitFields(value),
188
- board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
189
- source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
190
- sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
191
- title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
192
- url,
193
- stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
194
- ...metadata ? { metadata } : {}
195
- };
196
- }
197
- case "invokeSkill": {
198
- assertExactKeys(
199
- value,
200
- ["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
201
- "Factory invoke skill decision"
202
- );
203
- const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
204
- const precedingMessage = optionalBoundedString(
205
- value.precedingMessage,
206
- "Factory skill preceding message",
207
- MAX_MESSAGE_LENGTH
208
- );
209
- return {
210
- type,
211
- ...commonCommitFields(value),
212
- role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
213
- skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
214
- ...args ? { arguments: args } : {},
215
- ...precedingMessage ? { precedingMessage } : {}
216
- };
217
- }
218
- case "sendMessage": {
219
- assertExactKeys(
220
- value,
221
- ["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
222
- "Factory send message decision"
223
- );
224
- const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
225
- const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
226
- if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
227
- throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
228
- }
229
- return {
230
- type,
231
- ...commonCommitFields(value),
232
- role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
233
- message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
234
- ...priority ? { priority } : {},
235
- ...idleBehavior ? { idleBehavior } : {},
236
- ...value.prepareBinding === true ? { prepareBinding: true } : {}
237
- };
238
- }
239
- case "notify": {
240
- assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
241
- const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
242
- const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
243
- return {
244
- type,
245
- ...commonCommitFields(value),
246
- title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
247
- ...body ? { body } : {},
248
- ...level ? { level } : {}
249
- };
250
- }
251
- default:
252
- throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
253
- }
254
- }
255
- function validateFactoryRuleDecisions(values, causalDepth = 0) {
256
- if (values.length > MAX_JSON_COLLECTION_SIZE) {
257
- throw new FactoryRuleValidationError("Factory rule produced too many decisions.");
258
- }
259
- const decisions = [];
260
- for (const value of values) {
261
- const decision = validateFactoryRuleDecision(value, causalDepth);
262
- if (decision.type === "reject") {
263
- throw new FactoryRuleValidationError("A rejection cannot be persisted with commit decisions.");
264
- }
265
- decisions.push(decision);
266
- }
267
- const keys = decisions.map((decision) => decision.idempotencyKey);
268
- if (new Set(keys).size !== keys.length) {
269
- throw new FactoryRuleValidationError("Factory decisions require unique idempotency keys.");
270
- }
271
- return decisions;
272
- }
273
-
274
- // src/rules/processor.ts
275
- var STATE_ID = "factory-phase";
276
- var RULE_TIMEOUT_MS = 5e3;
277
- var TRANSCRIPT_PAGE_SIZE = 50;
278
- var MAX_LINKED_ITEMS = 5;
279
- var PHASE_LABELS = {
280
- intake: "Intake",
281
- triage: "Investigating",
282
- planning: "Planning",
283
- execute: "Building",
284
- review: "Reviewing",
285
- done: "Done",
286
- canceled: "Canceled"
6
+ //#region src/rules/processor.ts
7
+ const STATE_ID = "factory-phase";
8
+ const RULE_TIMEOUT_MS = 5e3;
9
+ const TRANSCRIPT_PAGE_SIZE = 50;
10
+ const MAX_LINKED_ITEMS = 5;
11
+ const PHASE_LABELS = {
12
+ intake: "Intake",
13
+ triage: "Investigating",
14
+ planning: "Planning",
15
+ execute: "Building",
16
+ review: "Reviewing",
17
+ done: "Done",
18
+ canceled: "Canceled"
287
19
  };
288
20
  function workItemSource(item) {
289
- if (!item.externalSource) return "manual";
290
- if (item.externalSource.integrationId === "linear") return "linear-issue";
291
- return item.externalSource.type === "pull-request" ? "github-pr" : "github-issue";
21
+ if (!item.externalSource) return "manual";
22
+ if (item.externalSource.integrationId === "linear") return "linear-issue";
23
+ return item.externalSource.type === "pull-request" ? "github-pr" : "github-issue";
292
24
  }
293
25
  function workItemSourceKey(item) {
294
- const source = item.externalSource;
295
- return source ? `${source.integrationId}:${source.type}:${source.externalId}` : null;
26
+ const source = item.externalSource;
27
+ return source ? `${source.integrationId}:${source.type}:${source.externalId}` : null;
296
28
  }
297
29
  function boardForItem(item) {
298
- return item.externalSource?.type === "pull-request" ? "review" : "work";
30
+ return item.externalSource?.type === "pull-request" ? "review" : "work";
299
31
  }
300
32
  function boundedError(value) {
301
- const message = value instanceof Error ? value.message : typeof value === "string" ? value : "Tool execution failed.";
302
- return { message: message.slice(0, 2e3) };
33
+ return { message: (value instanceof Error ? value.message : typeof value === "string" ? value : "Tool execution failed.").slice(0, 2e3) };
303
34
  }
304
35
  function boundedResult(value) {
305
- try {
306
- return normalizeFactoryRuleJsonValue(value);
307
- } catch {
308
- return { message: "Tool result was not serializable." };
309
- }
36
+ try {
37
+ return normalizeFactoryRuleJsonValue(value);
38
+ } catch {
39
+ return { message: "Tool result was not serializable." };
40
+ }
310
41
  }
311
42
  function messageParts(message) {
312
- const content = message.content;
313
- if (Array.isArray(content)) return content;
314
- if (Array.isArray(content?.parts)) return content.parts;
315
- return Array.isArray(content?.toolInvocations) ? content.toolInvocations : [];
43
+ const content = message.content;
44
+ if (Array.isArray(content)) return content;
45
+ if (Array.isArray(content?.parts)) return content.parts;
46
+ return Array.isArray(content?.toolInvocations) ? content.toolInvocations : [];
316
47
  }
317
48
  function completedStepToolCallIds(steps) {
318
- const ids = /* @__PURE__ */ new Set();
319
- for (const rawStep of steps) {
320
- if (!rawStep || typeof rawStep !== "object") continue;
321
- const toolResults = rawStep.toolResults;
322
- if (!Array.isArray(toolResults)) continue;
323
- for (const rawResult of toolResults) {
324
- if (!rawResult || typeof rawResult !== "object") continue;
325
- const toolCallId = rawResult.toolCallId;
326
- if (typeof toolCallId === "string") ids.add(toolCallId);
327
- }
328
- }
329
- return ids;
49
+ const ids = /* @__PURE__ */ new Set();
50
+ for (const rawStep of steps) {
51
+ if (!rawStep || typeof rawStep !== "object") continue;
52
+ const toolResults = rawStep.toolResults;
53
+ if (!Array.isArray(toolResults)) continue;
54
+ for (const rawResult of toolResults) {
55
+ if (!rawResult || typeof rawResult !== "object") continue;
56
+ const toolCallId = rawResult.toolCallId;
57
+ if (typeof toolCallId === "string") ids.add(toolCallId);
58
+ }
59
+ }
60
+ return ids;
330
61
  }
331
62
  function completedToolResults(message) {
332
- if (message.role !== "assistant") return [];
333
- const createdAt = message.createdAt instanceof Date ? message.createdAt : new Date(message.createdAt);
334
- const completed = [];
335
- for (const rawPart of messageParts(message)) {
336
- if (!rawPart || typeof rawPart !== "object") continue;
337
- const part = rawPart;
338
- const invocation = part.type === "tool-invocation" && part.toolInvocation && typeof part.toolInvocation === "object" ? part.toolInvocation : part;
339
- const state = invocation.state;
340
- if (state !== "result" && state !== "error") continue;
341
- const toolCallId = invocation.toolCallId;
342
- const toolName = invocation.toolName ?? invocation.name;
343
- if (typeof toolCallId !== "string" || typeof toolName !== "string") continue;
344
- completed.push({
345
- assistantMessageId: message.id,
346
- messageCreatedAt: createdAt,
347
- toolCallId,
348
- toolName: toolName.slice(0, 256),
349
- input: boundedResult(invocation.args ?? {}),
350
- status: state === "error" ? "error" : "success",
351
- value: state === "error" ? boundedError(invocation.result ?? invocation.error) : boundedResult(invocation.result)
352
- });
353
- }
354
- return completed;
63
+ if (message.role !== "assistant") return [];
64
+ const createdAt = message.createdAt instanceof Date ? message.createdAt : new Date(message.createdAt);
65
+ const completed = [];
66
+ for (const rawPart of messageParts(message)) {
67
+ if (!rawPart || typeof rawPart !== "object") continue;
68
+ const part = rawPart;
69
+ const invocation = part.type === "tool-invocation" && part.toolInvocation && typeof part.toolInvocation === "object" ? part.toolInvocation : part;
70
+ const state = invocation.state;
71
+ if (state !== "result" && state !== "error") continue;
72
+ const toolCallId = invocation.toolCallId;
73
+ const toolName = invocation.toolName ?? invocation.name;
74
+ if (typeof toolCallId !== "string" || typeof toolName !== "string") continue;
75
+ completed.push({
76
+ assistantMessageId: message.id,
77
+ messageCreatedAt: createdAt,
78
+ toolCallId,
79
+ toolName: toolName.slice(0, 256),
80
+ input: boundedResult(invocation.args ?? {}),
81
+ status: state === "error" ? "error" : "success",
82
+ value: state === "error" ? boundedError(invocation.result ?? invocation.error) : boundedResult(invocation.result)
83
+ });
84
+ }
85
+ return completed;
355
86
  }
356
87
  function currentCompletedToolMessage(messages, toolCallIds) {
357
- for (let index = messages.length - 1; index >= 0; index -= 1) {
358
- const message = messages[index];
359
- if (completedToolResults(message).some((result) => toolCallIds.has(result.toolCallId))) return message;
360
- }
361
- return void 0;
88
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
89
+ const message = messages[index];
90
+ if (completedToolResults(message).some((result) => toolCallIds.has(result.toolCallId))) return message;
91
+ }
362
92
  }
363
93
  function phaseCacheKey(value, linked) {
364
- return createHash("sha256").update(
365
- JSON.stringify({
366
- ...value,
367
- linked: linked.map((item) => [item.id, item.revision, item.stages[0]])
368
- })
369
- ).digest("hex");
94
+ return createHash("sha256").update(JSON.stringify({
95
+ ...value,
96
+ linked: linked.map((item) => [
97
+ item.id,
98
+ item.revision,
99
+ item.stages[0]
100
+ ])
101
+ })).digest("hex");
370
102
  }
371
103
  function escapeText(value) {
372
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
104
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
373
105
  }
374
106
  function phaseFromSignal(signal) {
375
- return signal?.metadata?.value?.phase;
107
+ return (signal?.metadata?.value)?.phase;
376
108
  }
377
109
  function latestPhase(args) {
378
- for (const signal of [...args.activeStateSignals].reverse()) {
379
- const phase = phaseFromSignal(signal);
380
- if (phase) return phase;
381
- }
382
- return phaseFromSignal(args.lastSnapshot);
110
+ for (const signal of [...args.activeStateSignals].reverse()) {
111
+ const phase = phaseFromSignal(signal);
112
+ if (phase) return phase;
113
+ }
114
+ return phaseFromSignal(args.lastSnapshot);
383
115
  }
384
116
  async function withRuleTimeout(operation) {
385
- let timer;
386
- try {
387
- return await Promise.race([
388
- operation,
389
- new Promise((_, reject) => {
390
- timer = setTimeout(() => reject(new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
391
- })
392
- ]);
393
- } finally {
394
- if (timer) clearTimeout(timer);
395
- }
117
+ let timer;
118
+ try {
119
+ return await Promise.race([operation, new Promise((_, reject) => {
120
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
121
+ })]);
122
+ } finally {
123
+ if (timer) clearTimeout(timer);
124
+ }
396
125
  }
397
126
  var FactoryPhaseStateProcessor = class {
398
- constructor(options) {
399
- this.options = options;
400
- }
401
- options;
402
- id = STATE_ID;
403
- stateId = STATE_ID;
404
- async processInputStep(args) {
405
- const address = getFactorySessionCoordinates(args.requestContext);
406
- if (!address) return;
407
- const binding = await this.options.storage.findRunBindingBySession(address);
408
- if (!binding || binding.status !== "active") return;
409
- const completedToolCallIds = completedStepToolCallIds(args.steps);
410
- const completedMessage = currentCompletedToolMessage(args.messages, completedToolCallIds);
411
- if (completedMessage) {
412
- await this.ingestMessages(binding, [completedMessage], completedToolCallIds);
413
- }
414
- }
415
- async computeStateSignal(args) {
416
- const address = getFactorySessionCoordinates(args.requestContext);
417
- if (!address) return;
418
- const binding = await this.options.storage.findRunBindingBySession(address);
419
- const prior = latestPhase(args);
420
- const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;
421
- if (!binding) return;
422
- if (binding.status !== "active") {
423
- if (!hasBase || prior?.status !== "active") return;
424
- return {
425
- id: STATE_ID,
426
- cacheKey: `factory:none:${prior.bindingId ?? "revoked"}`,
427
- mode: "snapshot",
428
- tagName: "factory-phase",
429
- contents: "\n",
430
- value: { phase: { status: "none" } },
431
- attributes: { status: "none" },
432
- metadata: { value: { phase: { status: "none" } } }
433
- };
434
- }
435
- const item = await this.options.storage.get({ orgId: binding.orgId, id: binding.workItemId });
436
- if (!item || item.stages.length !== 1 || !FACTORY_RULE_STAGES.includes(item.stages[0])) return;
437
- const allItems = await this.options.storage.list({
438
- orgId: binding.orgId,
439
- factoryProjectId: binding.factoryProjectId
440
- });
441
- const linked = allItems.filter((candidate) => candidate.parentWorkItemId === item.id || item.parentWorkItemId === candidate.id).slice(0, MAX_LINKED_ITEMS);
442
- const board = boardForItem(item);
443
- const stage = item.stages[0];
444
- const value = {
445
- status: "active",
446
- bindingId: binding.id,
447
- itemId: item.id,
448
- revision: item.revision,
449
- stage,
450
- role: binding.role,
451
- board,
452
- ruleSetVersion: this.options.rules.version
453
- };
454
- const cacheKey = phaseCacheKey(value, linked);
455
- if (hasBase && (args.tracking?.currentCacheKey ?? args.lastSnapshot?.metadata?.state?.cacheKey) === cacheKey)
456
- return;
457
- const linkedText = linked.length ? `
458
- Linked items: ${linked.map((candidate) => `${workItemSource(candidate)} ${candidate.title}`).join("; ")}` : "";
459
- const snapshotContents = `Factory ${board} phase: ${PHASE_LABELS[stage]} (${escapeText(stage)})
460
- Work item: ${escapeText(item.title)} (${item.id})
461
- Role: ${escapeText(binding.role)}
462
- Revision: ${item.revision}
463
- Rules: ${escapeText(this.options.rules.version)}
464
- Use factory_transition_work_item with expectedRevision ${item.revision} to request a phase change.${escapeText(linkedText)}`;
465
- const isDelta = hasBase && prior?.status === "active";
466
- return {
467
- id: STATE_ID,
468
- cacheKey,
469
- mode: isDelta ? "delta" : "snapshot",
470
- tagName: "factory-phase",
471
- contents: isDelta ? `Factory phase update:
472
- ${snapshotContents}` : snapshotContents,
473
- value: { phase: value },
474
- ...isDelta ? { delta: { phase: value } } : {},
475
- attributes: { status: "active", board, stage, role: binding.role, revision: item.revision },
476
- metadata: { value: { phase: value } }
477
- };
478
- }
479
- async reconcileAllBoundThreads() {
480
- if (!this.options.messageReader) return;
481
- const bindings = await this.options.storage.listActiveRunBindings();
482
- for (const binding of bindings) await this.reconcileBinding(binding);
483
- }
484
- async reconcileBinding(binding) {
485
- const reader = this.options.messageReader;
486
- if (!reader || binding.status !== "active") return;
487
- const cursor = await this.options.storage.getToolResultCursor(binding.orgId, binding.factoryProjectId, binding.id);
488
- let page = 0;
489
- while (true) {
490
- const result = await reader.listMessages({
491
- threadId: binding.threadId,
492
- resourceId: binding.resourceId,
493
- page,
494
- perPage: TRANSCRIPT_PAGE_SIZE,
495
- ...cursor ? { filter: { dateRange: { start: cursor.lastMessageCreatedAt } } } : {},
496
- orderBy: { field: "createdAt", direction: "ASC" }
497
- });
498
- await this.ingestMessages(binding, result.messages);
499
- const last = result.messages.at(-1);
500
- if (last) {
501
- await this.options.storage.advanceToolResultCursor({
502
- bindingId: binding.id,
503
- orgId: binding.orgId,
504
- factoryProjectId: binding.factoryProjectId,
505
- lastMessageId: last.id,
506
- lastMessageCreatedAt: last.createdAt instanceof Date ? last.createdAt : new Date(last.createdAt),
507
- updatedAt: /* @__PURE__ */ new Date()
508
- });
509
- }
510
- if (!result.hasMore) break;
511
- page += 1;
512
- }
513
- }
514
- async ingestMessages(binding, messages, toolCallIds) {
515
- const item = await this.options.storage.get({ orgId: binding.orgId, id: binding.workItemId });
516
- if (!item || item.stages.length !== 1 || !FACTORY_RULE_STAGES.includes(item.stages[0])) return;
517
- for (const message of messages) {
518
- for (const toolResult of completedToolResults(message)) {
519
- if (toolCallIds && !toolCallIds.has(toolResult.toolCallId)) continue;
520
- try {
521
- await this.options.recordPullRequestProvenance?.({
522
- binding,
523
- item,
524
- assistantMessageId: toolResult.assistantMessageId,
525
- toolCallId: toolResult.toolCallId,
526
- toolName: toolResult.toolName,
527
- toolInput: toolResult.input,
528
- toolResult: toolResult.value,
529
- status: toolResult.status
530
- });
531
- } catch {
532
- }
533
- await this.ingestToolResult(binding, item, toolResult);
534
- }
535
- }
536
- }
537
- async ingestToolResult(binding, item, toolResult) {
538
- const rule = resolveFactoryToolRule(this.options.rules, toolResult.toolName);
539
- if (!rule) return;
540
- const ingressId = JSON.stringify([
541
- binding.id,
542
- binding.threadId,
543
- toolResult.assistantMessageId,
544
- toolResult.toolCallId
545
- ]);
546
- const prior = await this.options.storage.getTransitionResultByIngress(
547
- binding.orgId,
548
- binding.factoryProjectId,
549
- ingressId
550
- );
551
- if (prior) return;
552
- const board = boardForItem(item);
553
- const context = {
554
- tenant: { orgId: binding.orgId, projectId: binding.factoryProjectId },
555
- actor: { type: "agent", bindingId: binding.id, role: binding.role },
556
- ingress: { type: "toolResult", id: ingressId },
557
- cause: `Completed ${toolResult.toolName}`,
558
- causalChain: [],
559
- ruleSetVersion: this.options.rules.version,
560
- item: {
561
- id: item.id,
562
- source: workItemSource(item),
563
- sourceKey: workItemSourceKey(item),
564
- parentWorkItemId: item.parentWorkItemId,
565
- title: item.title,
566
- url: item.externalSource?.url ?? null,
567
- stages: item.stages
568
- },
569
- board,
570
- itemRevision: item.revision,
571
- toolName: toolResult.toolName,
572
- threadId: binding.threadId,
573
- assistantMessageId: toolResult.assistantMessageId,
574
- toolCallId: toolResult.toolCallId,
575
- result: { status: toolResult.status, value: toolResult.value }
576
- };
577
- let decision = void 0;
578
- let decisions = [];
579
- let outcome = { status: "accepted" };
580
- try {
581
- decision = await withRuleTimeout(Promise.resolve(rule(Object.freeze(context))));
582
- if (decision?.type === "reject") {
583
- outcome = { status: "rejected", code: decision.code, reason: decision.reason };
584
- } else if (decision) {
585
- decisions = validateFactoryRuleDecisions([decision]);
586
- }
587
- } catch (error) {
588
- const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
589
- outcome = {
590
- status: "rejected",
591
- code: timedOut ? "timeout" : "rule_error",
592
- reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory tool-result rule failed."
593
- };
594
- }
595
- const committed = await this.options.storage.commitRuleEvaluation({
596
- orgId: binding.orgId,
597
- factoryProjectId: binding.factoryProjectId,
598
- workItemId: item.id,
599
- ingress: { identity: ingressId, triggerType: "tool.result" },
600
- ruleSetVersion: this.options.rules.version,
601
- expectedRevision: item.revision,
602
- actor: { ...context.actor },
603
- outcome,
604
- decisions: decisions.map((entry) => ({ ...entry })),
605
- causalChain: [],
606
- now: /* @__PURE__ */ new Date()
607
- });
608
- if (committed.status !== "committed" || !this.options.transitionService) return;
609
- for (const entry of decisions) {
610
- if (entry.type !== "transition") continue;
611
- await this.options.transitionService.transition({
612
- orgId: binding.orgId,
613
- factoryProjectId: binding.factoryProjectId,
614
- workItemId: item.id,
615
- board: entry.board,
616
- stage: entry.stage,
617
- expectedRevision: item.revision,
618
- actor: { type: "system", id: "factory-tool-result-rule" },
619
- ingress: { type: "rule", identity: `decision:${entry.idempotencyKey}` },
620
- cause: "tool_result_rule",
621
- causalChain: [{ ingressId, decisionType: entry.type }]
622
- });
623
- }
624
- }
625
- };
626
- export {
627
- FactoryPhaseStateProcessor
127
+ options;
128
+ id = STATE_ID;
129
+ stateId = STATE_ID;
130
+ constructor(options) {
131
+ this.options = options;
132
+ }
133
+ async processInputStep(args) {
134
+ const address = getFactorySessionCoordinates(args.requestContext);
135
+ if (!address) return;
136
+ const binding = await this.options.storage.findRunBindingBySession(address);
137
+ if (!binding || binding.status !== "active") return;
138
+ const completedToolCallIds = completedStepToolCallIds(args.steps);
139
+ const completedMessage = currentCompletedToolMessage(args.messages, completedToolCallIds);
140
+ if (completedMessage) await this.ingestMessages(binding, [completedMessage], completedToolCallIds);
141
+ }
142
+ async computeStateSignal(args) {
143
+ const address = getFactorySessionCoordinates(args.requestContext);
144
+ if (!address) return;
145
+ const binding = await this.options.storage.findRunBindingBySession(address);
146
+ const prior = latestPhase(args);
147
+ const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;
148
+ if (!binding) return;
149
+ if (binding.status !== "active") {
150
+ if (!hasBase || prior?.status !== "active") return;
151
+ return {
152
+ id: STATE_ID,
153
+ cacheKey: `factory:none:${prior.bindingId ?? "revoked"}`,
154
+ mode: "snapshot",
155
+ tagName: "factory-phase",
156
+ contents: "\n",
157
+ value: { phase: { status: "none" } },
158
+ attributes: { status: "none" },
159
+ metadata: { value: { phase: { status: "none" } } }
160
+ };
161
+ }
162
+ const item = await this.options.storage.get({
163
+ orgId: binding.orgId,
164
+ id: binding.workItemId
165
+ });
166
+ if (!item || item.stages.length !== 1 || !FACTORY_RULE_STAGES.includes(item.stages[0])) return;
167
+ const linked = (await this.options.storage.list({
168
+ orgId: binding.orgId,
169
+ factoryProjectId: binding.factoryProjectId
170
+ })).filter((candidate) => candidate.parentWorkItemId === item.id || item.parentWorkItemId === candidate.id).slice(0, MAX_LINKED_ITEMS);
171
+ const board = boardForItem(item);
172
+ const stage = item.stages[0];
173
+ const value = {
174
+ status: "active",
175
+ bindingId: binding.id,
176
+ itemId: item.id,
177
+ revision: item.revision,
178
+ stage,
179
+ role: binding.role,
180
+ board,
181
+ ruleSetVersion: this.options.rules.version
182
+ };
183
+ const cacheKey = phaseCacheKey(value, linked);
184
+ if (hasBase && (args.tracking?.currentCacheKey ?? args.lastSnapshot?.metadata?.state?.cacheKey) === cacheKey) return;
185
+ const linkedText = linked.length ? `\nLinked items: ${linked.map((candidate) => `${workItemSource(candidate)} ${candidate.title}`).join("; ")}` : "";
186
+ const snapshotContents = `Factory ${board} phase: ${PHASE_LABELS[stage]} (${escapeText(stage)})\nWork item: ${escapeText(item.title)} (${item.id})\nRole: ${escapeText(binding.role)}\nRevision: ${item.revision}\nRules: ${escapeText(this.options.rules.version)}\nUse factory_transition_work_item with expectedRevision ${item.revision} to request a phase change.${escapeText(linkedText)}`;
187
+ const isDelta = hasBase && prior?.status === "active";
188
+ return {
189
+ id: STATE_ID,
190
+ cacheKey,
191
+ mode: isDelta ? "delta" : "snapshot",
192
+ tagName: "factory-phase",
193
+ contents: isDelta ? `Factory phase update:\n${snapshotContents}` : snapshotContents,
194
+ value: { phase: value },
195
+ ...isDelta ? { delta: { phase: value } } : {},
196
+ attributes: {
197
+ status: "active",
198
+ board,
199
+ stage,
200
+ role: binding.role,
201
+ revision: item.revision
202
+ },
203
+ metadata: { value: { phase: value } }
204
+ };
205
+ }
206
+ async reconcileAllBoundThreads() {
207
+ if (!this.options.messageReader) return;
208
+ const bindings = await this.options.storage.listActiveRunBindings();
209
+ for (const binding of bindings) await this.reconcileBinding(binding);
210
+ }
211
+ async reconcileBinding(binding) {
212
+ const reader = this.options.messageReader;
213
+ if (!reader || binding.status !== "active") return;
214
+ const cursor = await this.options.storage.getToolResultCursor(binding.orgId, binding.factoryProjectId, binding.id);
215
+ let page = 0;
216
+ while (true) {
217
+ const result = await reader.listMessages({
218
+ threadId: binding.threadId,
219
+ resourceId: binding.resourceId,
220
+ page,
221
+ perPage: TRANSCRIPT_PAGE_SIZE,
222
+ ...cursor ? { filter: { dateRange: { start: cursor.lastMessageCreatedAt } } } : {},
223
+ orderBy: {
224
+ field: "createdAt",
225
+ direction: "ASC"
226
+ }
227
+ });
228
+ await this.ingestMessages(binding, result.messages);
229
+ const last = result.messages.at(-1);
230
+ if (last) await this.options.storage.advanceToolResultCursor({
231
+ bindingId: binding.id,
232
+ orgId: binding.orgId,
233
+ factoryProjectId: binding.factoryProjectId,
234
+ lastMessageId: last.id,
235
+ lastMessageCreatedAt: last.createdAt instanceof Date ? last.createdAt : new Date(last.createdAt),
236
+ updatedAt: /* @__PURE__ */ new Date()
237
+ });
238
+ if (!result.hasMore) break;
239
+ page += 1;
240
+ }
241
+ }
242
+ async ingestMessages(binding, messages, toolCallIds) {
243
+ const item = await this.options.storage.get({
244
+ orgId: binding.orgId,
245
+ id: binding.workItemId
246
+ });
247
+ if (!item || item.stages.length !== 1 || !FACTORY_RULE_STAGES.includes(item.stages[0])) return;
248
+ for (const message of messages) for (const toolResult of completedToolResults(message)) {
249
+ if (toolCallIds && !toolCallIds.has(toolResult.toolCallId)) continue;
250
+ try {
251
+ await this.options.recordPullRequestProvenance?.({
252
+ binding,
253
+ item,
254
+ assistantMessageId: toolResult.assistantMessageId,
255
+ toolCallId: toolResult.toolCallId,
256
+ toolName: toolResult.toolName,
257
+ toolInput: toolResult.input,
258
+ toolResult: toolResult.value,
259
+ status: toolResult.status
260
+ });
261
+ } catch {}
262
+ await this.ingestToolResult(binding, item, toolResult);
263
+ }
264
+ }
265
+ async ingestToolResult(binding, item, toolResult) {
266
+ const rule = resolveFactoryToolRule(this.options.rules, toolResult.toolName);
267
+ if (!rule) return;
268
+ const ingressId = JSON.stringify([
269
+ binding.id,
270
+ binding.threadId,
271
+ toolResult.assistantMessageId,
272
+ toolResult.toolCallId
273
+ ]);
274
+ if (await this.options.storage.getTransitionResultByIngress(binding.orgId, binding.factoryProjectId, ingressId)) return;
275
+ const board = boardForItem(item);
276
+ const context = {
277
+ tenant: {
278
+ orgId: binding.orgId,
279
+ projectId: binding.factoryProjectId
280
+ },
281
+ actor: {
282
+ type: "agent",
283
+ bindingId: binding.id,
284
+ role: binding.role
285
+ },
286
+ ingress: {
287
+ type: "toolResult",
288
+ id: ingressId
289
+ },
290
+ cause: `Completed ${toolResult.toolName}`,
291
+ causalChain: [],
292
+ ruleSetVersion: this.options.rules.version,
293
+ item: {
294
+ id: item.id,
295
+ source: workItemSource(item),
296
+ sourceKey: workItemSourceKey(item),
297
+ parentWorkItemId: item.parentWorkItemId,
298
+ title: item.title,
299
+ url: item.externalSource?.url ?? null,
300
+ stages: item.stages
301
+ },
302
+ board,
303
+ itemRevision: item.revision,
304
+ toolName: toolResult.toolName,
305
+ threadId: binding.threadId,
306
+ assistantMessageId: toolResult.assistantMessageId,
307
+ toolCallId: toolResult.toolCallId,
308
+ result: {
309
+ status: toolResult.status,
310
+ value: toolResult.value
311
+ }
312
+ };
313
+ let decision = void 0;
314
+ let decisions = [];
315
+ let outcome = { status: "accepted" };
316
+ try {
317
+ decision = await withRuleTimeout(Promise.resolve(rule(Object.freeze(context))));
318
+ if (decision?.type === "reject") outcome = {
319
+ status: "rejected",
320
+ code: decision.code,
321
+ reason: decision.reason
322
+ };
323
+ else if (decision) decisions = validateFactoryRuleDecisions([decision]);
324
+ } catch (error) {
325
+ const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
326
+ outcome = {
327
+ status: "rejected",
328
+ code: timedOut ? "timeout" : "rule_error",
329
+ reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory tool-result rule failed."
330
+ };
331
+ }
332
+ if ((await this.options.storage.commitRuleEvaluation({
333
+ orgId: binding.orgId,
334
+ factoryProjectId: binding.factoryProjectId,
335
+ workItemId: item.id,
336
+ ingress: {
337
+ identity: ingressId,
338
+ triggerType: "tool.result"
339
+ },
340
+ ruleSetVersion: this.options.rules.version,
341
+ expectedRevision: item.revision,
342
+ actor: { ...context.actor },
343
+ outcome,
344
+ decisions: decisions.map((entry) => ({ ...entry })),
345
+ causalChain: [],
346
+ now: /* @__PURE__ */ new Date()
347
+ })).status !== "committed" || !this.options.transitionService) return;
348
+ for (const entry of decisions) {
349
+ if (entry.type !== "transition") continue;
350
+ await this.options.transitionService.transition({
351
+ orgId: binding.orgId,
352
+ factoryProjectId: binding.factoryProjectId,
353
+ workItemId: item.id,
354
+ board: entry.board,
355
+ stage: entry.stage,
356
+ expectedRevision: item.revision,
357
+ actor: {
358
+ type: "system",
359
+ id: "factory-tool-result-rule"
360
+ },
361
+ ingress: {
362
+ type: "rule",
363
+ identity: `decision:${entry.idempotencyKey}`
364
+ },
365
+ cause: "tool_result_rule",
366
+ causalChain: [{
367
+ ingressId,
368
+ decisionType: entry.type
369
+ }]
370
+ });
371
+ }
372
+ }
628
373
  };
374
+ //#endregion
375
+ export { FactoryPhaseStateProcessor };
376
+
629
377
  //# sourceMappingURL=processor.js.map