@mastra/factory 0.0.0 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE.md +30 -0
  3. package/dist/auth.d.ts +181 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +370 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/capabilities/connection.d.ts +9 -0
  8. package/dist/capabilities/connection.d.ts.map +1 -0
  9. package/dist/capabilities/connection.js +1 -0
  10. package/dist/capabilities/connection.js.map +1 -0
  11. package/dist/capabilities/intake.d.ts +93 -0
  12. package/dist/capabilities/intake.d.ts.map +1 -0
  13. package/dist/capabilities/intake.js +1 -0
  14. package/dist/capabilities/intake.js.map +1 -0
  15. package/dist/capabilities/version-control.d.ts +239 -0
  16. package/dist/capabilities/version-control.d.ts.map +1 -0
  17. package/dist/capabilities/version-control.js +1 -0
  18. package/dist/capabilities/version-control.js.map +1 -0
  19. package/dist/factory.d.ts +145 -0
  20. package/dist/factory.d.ts.map +1 -0
  21. package/dist/factory.js +38245 -0
  22. package/dist/factory.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +38245 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/integrations/base.d.ts +197 -0
  28. package/dist/integrations/base.d.ts.map +1 -0
  29. package/dist/integrations/base.js +1 -0
  30. package/dist/integrations/base.js.map +1 -0
  31. package/dist/integrations/github/config.d.ts +60 -0
  32. package/dist/integrations/github/config.d.ts.map +1 -0
  33. package/dist/integrations/github/config.js +28 -0
  34. package/dist/integrations/github/config.js.map +1 -0
  35. package/dist/integrations/github/factory-session.d.ts +12 -0
  36. package/dist/integrations/github/factory-session.d.ts.map +1 -0
  37. package/dist/integrations/github/factory-session.js +41 -0
  38. package/dist/integrations/github/factory-session.js.map +1 -0
  39. package/dist/integrations/github/integration.d.ts +191 -0
  40. package/dist/integrations/github/integration.d.ts.map +1 -0
  41. package/dist/integrations/github/integration.js +3001 -0
  42. package/dist/integrations/github/integration.js.map +1 -0
  43. package/dist/integrations/github/issue-triage.d.ts +27 -0
  44. package/dist/integrations/github/issue-triage.d.ts.map +1 -0
  45. package/dist/integrations/github/issue-triage.js +82 -0
  46. package/dist/integrations/github/issue-triage.js.map +1 -0
  47. package/dist/integrations/github/project-lock.d.ts +78 -0
  48. package/dist/integrations/github/project-lock.d.ts.map +1 -0
  49. package/dist/integrations/github/project-lock.js +70 -0
  50. package/dist/integrations/github/project-lock.js.map +1 -0
  51. package/dist/integrations/github/provenance.d.ts +27 -0
  52. package/dist/integrations/github/provenance.d.ts.map +1 -0
  53. package/dist/integrations/github/provenance.js +104 -0
  54. package/dist/integrations/github/provenance.js.map +1 -0
  55. package/dist/integrations/github/routes.d.ts +72 -0
  56. package/dist/integrations/github/routes.d.ts.map +1 -0
  57. package/dist/integrations/github/routes.js +1834 -0
  58. package/dist/integrations/github/routes.js.map +1 -0
  59. package/dist/integrations/github/sandbox.d.ts +268 -0
  60. package/dist/integrations/github/sandbox.d.ts.map +1 -0
  61. package/dist/integrations/github/sandbox.js +381 -0
  62. package/dist/integrations/github/sandbox.js.map +1 -0
  63. package/dist/integrations/github/session-subscriptions.d.ts +23 -0
  64. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
  65. package/dist/integrations/github/session-subscriptions.js +190 -0
  66. package/dist/integrations/github/session-subscriptions.js.map +1 -0
  67. package/dist/integrations/github/subscriptions.d.ts +54 -0
  68. package/dist/integrations/github/subscriptions.d.ts.map +1 -0
  69. package/dist/integrations/github/subscriptions.js +70 -0
  70. package/dist/integrations/github/subscriptions.js.map +1 -0
  71. package/dist/integrations/github/webhook.d.ts +87 -0
  72. package/dist/integrations/github/webhook.d.ts.map +1 -0
  73. package/dist/integrations/github/webhook.js +381 -0
  74. package/dist/integrations/github/webhook.js.map +1 -0
  75. package/dist/integrations/linear/agent-tools.d.ts +34 -0
  76. package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
  77. package/dist/integrations/linear/agent-tools.js +108 -0
  78. package/dist/integrations/linear/agent-tools.js.map +1 -0
  79. package/dist/integrations/linear/integration.d.ts +233 -0
  80. package/dist/integrations/linear/integration.d.ts.map +1 -0
  81. package/dist/integrations/linear/integration.js +962 -0
  82. package/dist/integrations/linear/integration.js.map +1 -0
  83. package/dist/integrations/linear/routes.d.ts +61 -0
  84. package/dist/integrations/linear/routes.d.ts.map +1 -0
  85. package/dist/integrations/linear/routes.js +242 -0
  86. package/dist/integrations/linear/routes.js.map +1 -0
  87. package/dist/integrations/linear/storage.d.ts +65 -0
  88. package/dist/integrations/linear/storage.d.ts.map +1 -0
  89. package/dist/integrations/linear/storage.js +1 -0
  90. package/dist/integrations/linear/storage.js.map +1 -0
  91. package/dist/integrations/platform/api-client.d.ts +24 -0
  92. package/dist/integrations/platform/api-client.d.ts.map +1 -0
  93. package/dist/integrations/platform/api-client.js +153 -0
  94. package/dist/integrations/platform/api-client.js.map +1 -0
  95. package/dist/integrations/platform/github/event-worker.d.ts +44 -0
  96. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
  97. package/dist/integrations/platform/github/event-worker.js +585 -0
  98. package/dist/integrations/platform/github/event-worker.js.map +1 -0
  99. package/dist/integrations/platform/github/integration.d.ts +36 -0
  100. package/dist/integrations/platform/github/integration.d.ts.map +1 -0
  101. package/dist/integrations/platform/github/integration.js +3253 -0
  102. package/dist/integrations/platform/github/integration.js.map +1 -0
  103. package/dist/integrations/platform/linear/integration.d.ts +46 -0
  104. package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
  105. package/dist/integrations/platform/linear/integration.js +891 -0
  106. package/dist/integrations/platform/linear/integration.js.map +1 -0
  107. package/dist/integrations/workos/integration.d.ts +39 -0
  108. package/dist/integrations/workos/integration.d.ts.map +1 -0
  109. package/dist/integrations/workos/integration.js +96 -0
  110. package/dist/integrations/workos/integration.js.map +1 -0
  111. package/dist/routes/config.d.ts +220 -0
  112. package/dist/routes/config.d.ts.map +1 -0
  113. package/dist/routes/config.js +770 -0
  114. package/dist/routes/config.js.map +1 -0
  115. package/dist/routes/fs.d.ts +105 -0
  116. package/dist/routes/fs.d.ts.map +1 -0
  117. package/dist/routes/fs.js +294 -0
  118. package/dist/routes/fs.js.map +1 -0
  119. package/dist/routes/intake.d.ts +23 -0
  120. package/dist/routes/intake.d.ts.map +1 -0
  121. package/dist/routes/intake.js +179 -0
  122. package/dist/routes/intake.js.map +1 -0
  123. package/dist/routes/oauth.d.ts +43 -0
  124. package/dist/routes/oauth.d.ts.map +1 -0
  125. package/dist/routes/oauth.js +25058 -0
  126. package/dist/routes/oauth.js.map +1 -0
  127. package/dist/routes/projects.d.ts +19 -0
  128. package/dist/routes/projects.d.ts.map +1 -0
  129. package/dist/routes/projects.js +395 -0
  130. package/dist/routes/projects.js.map +1 -0
  131. package/dist/routes/provider-credentials.d.ts +81 -0
  132. package/dist/routes/provider-credentials.d.ts.map +1 -0
  133. package/dist/routes/provider-credentials.js +68 -0
  134. package/dist/routes/provider-credentials.js.map +1 -0
  135. package/dist/routes/route.d.ts +48 -0
  136. package/dist/routes/route.d.ts.map +1 -0
  137. package/dist/routes/route.js +11 -0
  138. package/dist/routes/route.js.map +1 -0
  139. package/dist/routes/skills.d.ts +30 -0
  140. package/dist/routes/skills.d.ts.map +1 -0
  141. package/dist/routes/skills.js +181 -0
  142. package/dist/routes/skills.js.map +1 -0
  143. package/dist/routes/surface.d.ts +83 -0
  144. package/dist/routes/surface.d.ts.map +1 -0
  145. package/dist/routes/surface.js +28519 -0
  146. package/dist/routes/surface.js.map +1 -0
  147. package/dist/routes/tenant-credentials.d.ts +74 -0
  148. package/dist/routes/tenant-credentials.d.ts.map +1 -0
  149. package/dist/routes/tenant-credentials.js +181 -0
  150. package/dist/routes/tenant-credentials.js.map +1 -0
  151. package/dist/routes/work-items.d.ts +40 -0
  152. package/dist/routes/work-items.d.ts.map +1 -0
  153. package/dist/routes/work-items.js +902 -0
  154. package/dist/routes/work-items.js.map +1 -0
  155. package/dist/rules/binding-context.d.ts +5 -0
  156. package/dist/rules/binding-context.d.ts.map +1 -0
  157. package/dist/rules/binding-context.js +40 -0
  158. package/dist/rules/binding-context.js.map +1 -0
  159. package/dist/rules/defaults.d.ts +9 -0
  160. package/dist/rules/defaults.d.ts.map +1 -0
  161. package/dist/rules/defaults.js +334 -0
  162. package/dist/rules/defaults.js.map +1 -0
  163. package/dist/rules/dispatcher.d.ts +40 -0
  164. package/dist/rules/dispatcher.d.ts.map +1 -0
  165. package/dist/rules/dispatcher.js +695 -0
  166. package/dist/rules/dispatcher.js.map +1 -0
  167. package/dist/rules/github-service.d.ts +29 -0
  168. package/dist/rules/github-service.d.ts.map +1 -0
  169. package/dist/rules/github-service.js +474 -0
  170. package/dist/rules/github-service.js.map +1 -0
  171. package/dist/rules/index.d.ts +7 -0
  172. package/dist/rules/index.d.ts.map +1 -0
  173. package/dist/rules/index.js +583 -0
  174. package/dist/rules/index.js.map +1 -0
  175. package/dist/rules/linear-service.d.ts +27 -0
  176. package/dist/rules/linear-service.d.ts.map +1 -0
  177. package/dist/rules/linear-service.js +343 -0
  178. package/dist/rules/linear-service.js.map +1 -0
  179. package/dist/rules/processor.d.ts +54 -0
  180. package/dist/rules/processor.d.ts.map +1 -0
  181. package/dist/rules/processor.js +629 -0
  182. package/dist/rules/processor.js.map +1 -0
  183. package/dist/rules/resolve.d.ts +17 -0
  184. package/dist/rules/resolve.d.ts.map +1 -0
  185. package/dist/rules/resolve.js +27 -0
  186. package/dist/rules/resolve.js.map +1 -0
  187. package/dist/rules/start-coordinator.d.ts +58 -0
  188. package/dist/rules/start-coordinator.d.ts.map +1 -0
  189. package/dist/rules/start-coordinator.js +136 -0
  190. package/dist/rules/start-coordinator.js.map +1 -0
  191. package/dist/rules/tools.d.ts +10 -0
  192. package/dist/rules/tools.d.ts.map +1 -0
  193. package/dist/rules/tools.js +93 -0
  194. package/dist/rules/tools.js.map +1 -0
  195. package/dist/rules/transition-service.d.ts +32 -0
  196. package/dist/rules/transition-service.d.ts.map +1 -0
  197. package/dist/rules/transition-service.js +474 -0
  198. package/dist/rules/transition-service.js.map +1 -0
  199. package/dist/rules/types.d.ts +225 -0
  200. package/dist/rules/types.d.ts.map +1 -0
  201. package/dist/rules/types.js +33 -0
  202. package/dist/rules/types.js.map +1 -0
  203. package/dist/rules/validation.d.ts +11 -0
  204. package/dist/rules/validation.d.ts.map +1 -0
  205. package/dist/rules/validation.js +310 -0
  206. package/dist/rules/validation.js.map +1 -0
  207. package/dist/sandbox/fleet.d.ts +208 -0
  208. package/dist/sandbox/fleet.d.ts.map +1 -0
  209. package/dist/sandbox/fleet.js +259 -0
  210. package/dist/sandbox/fleet.js.map +1 -0
  211. package/dist/sandbox/reattach.d.ts +3 -0
  212. package/dist/sandbox/reattach.d.ts.map +1 -0
  213. package/dist/sandbox/reattach.js +9 -0
  214. package/dist/sandbox/reattach.js.map +1 -0
  215. package/dist/server-error.d.ts +12 -0
  216. package/dist/server-error.d.ts.map +1 -0
  217. package/dist/server-error.js +20 -0
  218. package/dist/server-error.js.map +1 -0
  219. package/dist/skills/service.d.ts +48 -0
  220. package/dist/skills/service.d.ts.map +1 -0
  221. package/dist/skills/service.js +45 -0
  222. package/dist/skills/service.js.map +1 -0
  223. package/dist/spa-static.d.ts +34 -0
  224. package/dist/spa-static.d.ts.map +1 -0
  225. package/dist/spa-static.js +73 -0
  226. package/dist/spa-static.js.map +1 -0
  227. package/dist/state-signing.d.ts +48 -0
  228. package/dist/state-signing.d.ts.map +1 -0
  229. package/dist/state-signing.js +48 -0
  230. package/dist/state-signing.js.map +1 -0
  231. package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
  232. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
  233. package/dist/storage/domains/audit/agent-audit.js +65 -0
  234. package/dist/storage/domains/audit/agent-audit.js.map +1 -0
  235. package/dist/storage/domains/audit/base.d.ts +140 -0
  236. package/dist/storage/domains/audit/base.d.ts.map +1 -0
  237. package/dist/storage/domains/audit/base.js +127 -0
  238. package/dist/storage/domains/audit/base.js.map +1 -0
  239. package/dist/storage/domains/audit/domain.d.ts +69 -0
  240. package/dist/storage/domains/audit/domain.d.ts.map +1 -0
  241. package/dist/storage/domains/audit/domain.js +165 -0
  242. package/dist/storage/domains/audit/domain.js.map +1 -0
  243. package/dist/storage/domains/credentials/base.d.ts +134 -0
  244. package/dist/storage/domains/credentials/base.d.ts.map +1 -0
  245. package/dist/storage/domains/credentials/base.js +257 -0
  246. package/dist/storage/domains/credentials/base.js.map +1 -0
  247. package/dist/storage/domains/intake/base.d.ts +38 -0
  248. package/dist/storage/domains/intake/base.d.ts.map +1 -0
  249. package/dist/storage/domains/intake/base.js +62 -0
  250. package/dist/storage/domains/intake/base.js.map +1 -0
  251. package/dist/storage/domains/integrations/base.d.ts +126 -0
  252. package/dist/storage/domains/integrations/base.d.ts.map +1 -0
  253. package/dist/storage/domains/integrations/base.js +252 -0
  254. package/dist/storage/domains/integrations/base.js.map +1 -0
  255. package/dist/storage/domains/model-packs/base.d.ts +49 -0
  256. package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
  257. package/dist/storage/domains/model-packs/base.js +95 -0
  258. package/dist/storage/domains/model-packs/base.js.map +1 -0
  259. package/dist/storage/domains/projects/base.d.ts +55 -0
  260. package/dist/storage/domains/projects/base.d.ts.map +1 -0
  261. package/dist/storage/domains/projects/base.js +99 -0
  262. package/dist/storage/domains/projects/base.js.map +1 -0
  263. package/dist/storage/domains/queue-health/base.d.ts +52 -0
  264. package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
  265. package/dist/storage/domains/queue-health/base.js +87 -0
  266. package/dist/storage/domains/queue-health/base.js.map +1 -0
  267. package/dist/storage/domains/source-control/base.d.ts +288 -0
  268. package/dist/storage/domains/source-control/base.d.ts.map +1 -0
  269. package/dist/storage/domains/source-control/base.js +722 -0
  270. package/dist/storage/domains/source-control/base.js.map +1 -0
  271. package/dist/storage/domains/source-control/inmemory.d.ts +158 -0
  272. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
  273. package/dist/storage/domains/source-control/inmemory.js +348 -0
  274. package/dist/storage/domains/source-control/inmemory.js.map +1 -0
  275. package/dist/storage/domains/work-items/base.d.ts +412 -0
  276. package/dist/storage/domains/work-items/base.d.ts.map +1 -0
  277. package/dist/storage/domains/work-items/base.js +1174 -0
  278. package/dist/storage/domains/work-items/base.js.map +1 -0
  279. package/dist/storage/domains/work-items/metrics.d.ts +101 -0
  280. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
  281. package/dist/storage/domains/work-items/metrics.js +203 -0
  282. package/dist/storage/domains/work-items/metrics.js.map +1 -0
  283. package/dist/workspace.d.ts +20 -0
  284. package/dist/workspace.d.ts.map +1 -0
  285. package/dist/workspace.js +364 -0
  286. package/dist/workspace.js.map +1 -0
  287. package/package.json +19 -19
@@ -0,0 +1,902 @@
1
+ // src/routes/work-items.ts
2
+ import { registerApiRoute } from "@mastra/core/server";
3
+
4
+ // src/rules/start-coordinator.ts
5
+ import { RequestContext } from "@mastra/core/request-context";
6
+ import { formatSkillActivation } from "@mastra/core/workspace";
7
+ var FactoryStartTransitionError = class extends Error {
8
+ result;
9
+ constructor(result) {
10
+ super(result.reason);
11
+ this.name = "FactoryStartTransitionError";
12
+ this.result = result;
13
+ }
14
+ };
15
+
16
+ // src/rules/types.ts
17
+ var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
18
+ var FACTORY_RULE_BOARDS = ["work", "review"];
19
+
20
+ // src/storage/domains/queue-health/base.ts
21
+ import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
22
+ var DEFAULT_QUEUE_HEALTH_CONFIG = {
23
+ thresholdsSeconds: [14400, 86400, 259200]
24
+ };
25
+ function assertValidThresholds(config) {
26
+ const t = config.thresholdsSeconds;
27
+ if (!Array.isArray(t) || t.length === 0 || t.some((v) => typeof v !== "number" || !Number.isFinite(v))) {
28
+ throw new Error("[QueueHealthStorage] thresholdsSeconds must be a non-empty array of finite numbers.");
29
+ }
30
+ for (let i = 1; i < t.length; i++) {
31
+ if (t[i] <= t[i - 1]) {
32
+ throw new Error("[QueueHealthStorage] thresholdsSeconds must be strictly ascending.");
33
+ }
34
+ }
35
+ }
36
+ function parseQueueHealthConfig(body) {
37
+ if (typeof body !== "object" || body === null) return null;
38
+ const config = body;
39
+ if (!Array.isArray(config.thresholdsSeconds)) return null;
40
+ try {
41
+ assertValidThresholds(config);
42
+ } catch {
43
+ return null;
44
+ }
45
+ return { thresholdsSeconds: [...config.thresholdsSeconds] };
46
+ }
47
+ function thresholdsOrDefault(config) {
48
+ return parseQueueHealthConfig(config)?.thresholdsSeconds ?? DEFAULT_QUEUE_HEALTH_CONFIG.thresholdsSeconds;
49
+ }
50
+
51
+ // src/storage/domains/work-items/base.ts
52
+ import { createHash } from "crypto";
53
+ import { FactoryStorageDomain as FactoryStorageDomain2, UniqueViolationError as UniqueViolationError2 } from "@mastra/core/storage";
54
+ var AUTOMATION_ACTORS = /* @__PURE__ */ new Set([
55
+ "factory",
56
+ "system",
57
+ "automation",
58
+ "factory-rule-dispatcher",
59
+ "factory-tool-result-rule"
60
+ ]);
61
+ function isAutomationActor(by) {
62
+ if (by === void 0) return false;
63
+ return AUTOMATION_ACTORS.has(by) || by.startsWith("agent:") || by.startsWith("github:");
64
+ }
65
+ var WorkItemRelationError = class extends Error {
66
+ code = "invalid_work_item_relation";
67
+ };
68
+
69
+ // src/storage/domains/work-items/metrics.ts
70
+ var DEFAULT_METRICS_WINDOW = 30;
71
+ var MAX_METRICS_WINDOW = 366;
72
+ var DAY_MS = 864e5;
73
+ var DONE_STAGE = "done";
74
+ var CANCELED_STAGE = "canceled";
75
+ var TERMINAL_STAGES = /* @__PURE__ */ new Set([DONE_STAGE, CANCELED_STAGE]);
76
+ var AGING_WIP_LIMIT = 10;
77
+ var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
78
+ var ZONED_DATETIME_RE = /(?:[Zz]|[+-]\d{2}:?\d{2})$/;
79
+ function parseRangeParam(value, boundary) {
80
+ if (typeof value !== "string" || value.length === 0) return void 0;
81
+ const dateOnly = DATE_ONLY_RE.test(value);
82
+ if (!dateOnly && !ZONED_DATETIME_RE.test(value)) return void 0;
83
+ const time = Date.parse(value);
84
+ if (Number.isNaN(time)) return void 0;
85
+ return boundary === "to" && dateOnly ? time + DAY_MS : time;
86
+ }
87
+ function utcDayStart(time) {
88
+ return Date.parse(`${utcDay(time)}T00:00:00Z`);
89
+ }
90
+ function parseMetricsRange(fromParam, toParam, now) {
91
+ const nowMs = now.getTime();
92
+ const endOfToday = utcDayStart(nowMs) + DAY_MS;
93
+ const requestedEnd = parseRangeParam(toParam, "to") ?? endOfToday;
94
+ const windowEnd = Math.min(requestedEnd, endOfToday);
95
+ const lastIncludedDay = utcDayStart(windowEnd - 1);
96
+ const defaultStart = lastIncludedDay - (DEFAULT_METRICS_WINDOW - 1) * DAY_MS;
97
+ const parsedFrom = parseRangeParam(fromParam, "from");
98
+ let windowStart = parsedFrom !== void 0 && parsedFrom < windowEnd ? parsedFrom : defaultStart;
99
+ const earliestStart = lastIncludedDay - (MAX_METRICS_WINDOW - 1) * DAY_MS;
100
+ if (windowStart < earliestStart) windowStart = earliestStart;
101
+ return { windowStart, windowEnd };
102
+ }
103
+ function parseTime(iso) {
104
+ const time = Date.parse(iso);
105
+ return Number.isNaN(time) ? 0 : time;
106
+ }
107
+ function percentile(samples, fraction) {
108
+ if (samples.length === 0) return null;
109
+ const sorted = [...samples].sort((a, b) => a - b);
110
+ const rank = Math.max(1, Math.ceil(fraction * sorted.length));
111
+ return sorted[rank - 1];
112
+ }
113
+ function utcDay(time) {
114
+ return new Date(time).toISOString().slice(0, 10);
115
+ }
116
+ function completedAt(item) {
117
+ if (!item.stages.includes(DONE_STAGE)) return void 0;
118
+ for (let i = item.stageHistory.length - 1; i >= 0; i--) {
119
+ const entry = item.stageHistory[i];
120
+ if (entry.stage === DONE_STAGE && entry.exitedAt === void 0) return parseTime(entry.enteredAt);
121
+ }
122
+ return void 0;
123
+ }
124
+ function openEntries(item) {
125
+ return item.stageHistory.filter(
126
+ (entry) => entry.exitedAt === void 0 && !TERMINAL_STAGES.has(entry.stage) && item.stages.includes(entry.stage)
127
+ );
128
+ }
129
+ function computeFactoryMetrics(items, opts) {
130
+ const { windowStart, windowEnd } = opts;
131
+ let earliest = Infinity;
132
+ for (const item of items) earliest = Math.min(earliest, item.createdAt.getTime());
133
+ const earliestItemAt = Number.isFinite(earliest) ? new Date(earliest).toISOString() : null;
134
+ const throughputByDay = /* @__PURE__ */ new Map();
135
+ const firstDay = utcDayStart(windowStart);
136
+ for (let day = firstDay; day < windowEnd; day += DAY_MS) {
137
+ throughputByDay.set(utcDay(day), 0);
138
+ }
139
+ const cycleSamples = [];
140
+ for (const item of items) {
141
+ const doneAt = completedAt(item);
142
+ if (doneAt === void 0 || doneAt < windowStart || doneAt >= windowEnd) continue;
143
+ const day = utcDay(doneAt);
144
+ throughputByDay.set(day, (throughputByDay.get(day) ?? 0) + 1);
145
+ cycleSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));
146
+ }
147
+ const durationsByStage = /* @__PURE__ */ new Map();
148
+ for (const item of items) {
149
+ for (const entry of item.stageHistory) {
150
+ if (entry.exitedAt === void 0 || TERMINAL_STAGES.has(entry.stage)) continue;
151
+ const exited = parseTime(entry.exitedAt);
152
+ if (exited < windowStart || exited >= windowEnd) continue;
153
+ const duration = Math.max(0, exited - parseTime(entry.enteredAt));
154
+ const samples = durationsByStage.get(entry.stage) ?? [];
155
+ samples.push(duration);
156
+ durationsByStage.set(entry.stage, samples);
157
+ }
158
+ }
159
+ const wipByStage = /* @__PURE__ */ new Map();
160
+ let wipTotal = 0;
161
+ const aging = [];
162
+ for (const item of items) {
163
+ for (const stage of item.stages) {
164
+ wipByStage.set(stage, (wipByStage.get(stage) ?? 0) + 1);
165
+ }
166
+ const inFlightStages = item.stages.filter((stage) => !TERMINAL_STAGES.has(stage));
167
+ if (inFlightStages.length === 0) continue;
168
+ wipTotal += 1;
169
+ const open = openEntries(item);
170
+ const oldest = open.reduce(
171
+ (best, entry) => !best || parseTime(entry.enteredAt) < parseTime(best.enteredAt) ? entry : best,
172
+ void 0
173
+ );
174
+ aging.push({
175
+ id: item.id,
176
+ title: item.title,
177
+ stage: oldest?.stage ?? inFlightStages[0],
178
+ enteredAt: oldest?.enteredAt ?? item.createdAt.toISOString(),
179
+ url: item.externalSource?.url ?? null
180
+ });
181
+ }
182
+ aging.sort((a, b) => parseTime(a.enteredAt) - parseTime(b.enteredAt));
183
+ const sourceCounts = /* @__PURE__ */ new Map();
184
+ let transitionsTotal = 0;
185
+ let transitionsHuman = 0;
186
+ for (const item of items) {
187
+ if (item.createdAt.getTime() >= windowStart && item.createdAt.getTime() < windowEnd) {
188
+ const source = item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : "manual";
189
+ sourceCounts.set(source, (sourceCounts.get(source) ?? 0) + 1);
190
+ }
191
+ for (const entry of item.stageHistory) {
192
+ const entered = parseTime(entry.enteredAt);
193
+ if (entered < windowStart || entered >= windowEnd) continue;
194
+ transitionsTotal += 1;
195
+ if (!isAutomationActor(entry.by)) transitionsHuman += 1;
196
+ }
197
+ }
198
+ const automationByStage = /* @__PURE__ */ new Map();
199
+ for (const item of items) {
200
+ const itemDone = completedAt(item) !== void 0;
201
+ const itemCanceled = item.stages.includes(CANCELED_STAGE);
202
+ for (let i = 0; i < item.stageHistory.length; i++) {
203
+ const entry = item.stageHistory[i];
204
+ if (entry.exitedAt === void 0 || TERMINAL_STAGES.has(entry.stage)) continue;
205
+ const exited = parseTime(entry.exitedAt);
206
+ if (exited < windowStart || exited >= windowEnd) continue;
207
+ let row = automationByStage.get(entry.stage);
208
+ if (!row) {
209
+ row = {
210
+ stage: entry.stage,
211
+ exits: 0,
212
+ automated: 0,
213
+ outcomes: { done: 0, canceled: 0, reworked: 0, inFlight: 0 }
214
+ };
215
+ automationByStage.set(entry.stage, row);
216
+ }
217
+ row.exits += 1;
218
+ const firstVisitIndex = item.stageHistory.findIndex((e) => e.stage === entry.stage);
219
+ if (firstVisitIndex !== i || !isAutomationActor(entry.by) || !isAutomationActor(entry.exitedBy)) continue;
220
+ row.automated += 1;
221
+ const reworked = item.stageHistory.some((e, j) => j > i && e.stage === entry.stage);
222
+ if (reworked) row.outcomes.reworked += 1;
223
+ else if (itemDone) row.outcomes.done += 1;
224
+ else if (itemCanceled) row.outcomes.canceled += 1;
225
+ else row.outcomes.inFlight += 1;
226
+ }
227
+ }
228
+ return {
229
+ windowDays: throughputByDay.size,
230
+ earliestItemAt,
231
+ throughput: [...throughputByDay.entries()].map(([date, count]) => ({ date, count })).sort((a, b) => a.date.localeCompare(b.date)),
232
+ cycleTime: {
233
+ medianMs: percentile(cycleSamples, 0.5),
234
+ p90Ms: percentile(cycleSamples, 0.9),
235
+ samples: cycleSamples.length
236
+ },
237
+ stageDurations: [...durationsByStage.entries()].map(([stage, samples]) => ({
238
+ stage,
239
+ medianMs: percentile(samples, 0.5),
240
+ samples: samples.length
241
+ })),
242
+ wip: [...wipByStage.entries()].map(([stage, count]) => ({ stage, count })),
243
+ wipTotal,
244
+ agingWip: aging.slice(0, AGING_WIP_LIMIT),
245
+ sourceMix: [...sourceCounts.entries()].map(([source, count]) => ({ source, count })).sort((a, b) => b.count - a.count),
246
+ transitions: { human: transitionsHuman, total: transitionsTotal },
247
+ stageAutomation: [...automationByStage.values()]
248
+ };
249
+ }
250
+
251
+ // src/routes/route.ts
252
+ var Route = class {
253
+ deps;
254
+ constructor(deps) {
255
+ this.deps = deps;
256
+ }
257
+ };
258
+
259
+ // src/routes/work-items.ts
260
+ function loose(c) {
261
+ return c;
262
+ }
263
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
264
+ var MAX_STAGES = 16;
265
+ var MAX_STAGE_LENGTH = 64;
266
+ var MAX_METADATA_BYTES = 16 * 1024;
267
+ function isRecord(value) {
268
+ return typeof value === "object" && value !== null && !Array.isArray(value);
269
+ }
270
+ function validStages(value) {
271
+ return Array.isArray(value) && value.length > 0 && value.length <= MAX_STAGES && value.every(
272
+ (stage) => typeof stage === "string" && stage.length <= MAX_STAGE_LENGTH && /^[a-z0-9][a-z0-9_-]*$/i.test(stage)
273
+ ) && new Set(value).size === value.length;
274
+ }
275
+ function validMetadata(value) {
276
+ if (value === null) return true;
277
+ if (!isRecord(value)) return false;
278
+ try {
279
+ return JSON.stringify(value).length <= MAX_METADATA_BYTES;
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+ function parseExternalSource(value) {
285
+ if (value === void 0 || value === null) return value;
286
+ if (!isRecord(value)) return void 0;
287
+ const { integrationId, type, externalId, url } = value;
288
+ if (typeof integrationId !== "string" || integrationId.length === 0 || integrationId.length > 128) return void 0;
289
+ if (typeof type !== "string" || type.length === 0 || type.length > 128) return void 0;
290
+ if (typeof externalId !== "string" || externalId.length === 0 || externalId.length > 512) return void 0;
291
+ if (url !== void 0 && (typeof url !== "string" || url.length > 2048)) return void 0;
292
+ return { integrationId, type, externalId, ...url !== void 0 ? { url } : {} };
293
+ }
294
+ function parseParentWorkItemId(value) {
295
+ if (value === null) return null;
296
+ if (typeof value !== "string" || !UUID_RE.test(value)) return void 0;
297
+ return value;
298
+ }
299
+ function parseSessions(value) {
300
+ if (!isRecord(value)) return void 0;
301
+ const out = {};
302
+ for (const [role, session] of Object.entries(value)) {
303
+ if (!role || role.length > 64 || !isRecord(session)) return void 0;
304
+ const { sessionId, branch, threadId } = session;
305
+ if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 512) return void 0;
306
+ if (typeof branch !== "string" || branch.length === 0 || branch.length > 512) return void 0;
307
+ if (typeof threadId !== "string" || threadId.length === 0 || threadId.length > 512) return void 0;
308
+ out[role] = { sessionId, branch, threadId };
309
+ }
310
+ return out;
311
+ }
312
+ function parseCreateWorkItem(body) {
313
+ if (!isRecord(body)) return null;
314
+ const { externalSource, title, stages, sessions, metadata } = body;
315
+ if (typeof title !== "string" || title.trim().length === 0 || title.length > 500) return null;
316
+ const hasParentWorkItemId = "parentWorkItemId" in body;
317
+ const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
318
+ if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
319
+ const parsedSource = parseExternalSource(externalSource);
320
+ if (externalSource !== void 0 && parsedSource === void 0) return null;
321
+ if (stages !== void 0 && !validStages(stages)) return null;
322
+ const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
323
+ if (sessions !== void 0 && parsedSessions === void 0) return null;
324
+ if (metadata !== void 0 && !validMetadata(metadata)) return null;
325
+ return {
326
+ title: title.trim(),
327
+ ...parsedSource !== void 0 ? { externalSource: parsedSource } : {},
328
+ ...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
329
+ ...stages !== void 0 ? { stages } : {},
330
+ ...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
331
+ ...metadata !== void 0 ? { metadata } : {}
332
+ };
333
+ }
334
+ function parseUpdateWorkItem(body) {
335
+ if (!isRecord(body)) return null;
336
+ const { title, stages, sessions, metadata } = body;
337
+ const hasParentWorkItemId = "parentWorkItemId" in body;
338
+ if (title === void 0 && stages === void 0 && sessions === void 0 && metadata === void 0 && !hasParentWorkItemId)
339
+ return null;
340
+ const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
341
+ if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
342
+ if (title !== void 0 && (typeof title !== "string" || title.trim().length === 0 || title.length > 500))
343
+ return null;
344
+ if (stages !== void 0 && !validStages(stages)) return null;
345
+ const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
346
+ if (sessions !== void 0 && parsedSessions === void 0) return null;
347
+ if (metadata !== void 0 && !validMetadata(metadata)) return null;
348
+ return {
349
+ ...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
350
+ ...title !== void 0 ? { title: title.trim() } : {},
351
+ ...stages !== void 0 ? { stages } : {},
352
+ ...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
353
+ ...metadata !== void 0 ? { metadata } : {}
354
+ };
355
+ }
356
+ async function readJson(c) {
357
+ try {
358
+ return await c.req.json();
359
+ } catch {
360
+ return void 0;
361
+ }
362
+ }
363
+ function patchedFields(patch) {
364
+ return Object.keys(patch).filter((key) => patch[key] !== void 0);
365
+ }
366
+ function boundedText(value, max) {
367
+ if (typeof value !== "string") return void 0;
368
+ const normalized = value.trim();
369
+ return normalized.length > 0 && normalized.length <= max ? normalized : void 0;
370
+ }
371
+ function parseTransitionBody(body) {
372
+ if (!isRecord(body)) return null;
373
+ const board = FACTORY_RULE_BOARDS.includes(body.board) ? body.board : void 0;
374
+ const stage = FACTORY_RULE_STAGES.includes(body.stage) ? body.stage : void 0;
375
+ const requestId = boundedText(body.requestId, 256);
376
+ const cause = boundedText(body.cause, 256);
377
+ if (!board || !stage || !requestId || !UUID_RE.test(requestId) || !cause || !Number.isInteger(body.expectedRevision) || Number(body.expectedRevision) < 1) {
378
+ return null;
379
+ }
380
+ return {
381
+ board,
382
+ stage,
383
+ expectedRevision: Number(body.expectedRevision),
384
+ ingress: { type: "human", identity: requestId },
385
+ cause
386
+ };
387
+ }
388
+ function parseInvocation(value) {
389
+ if (value === void 0) return void 0;
390
+ if (!isRecord(value)) return null;
391
+ if (value.type === "prompt") {
392
+ const prompt = boundedText(value.prompt, 16384);
393
+ return prompt ? { type: "prompt", prompt } : null;
394
+ }
395
+ if (value.type === "skill") {
396
+ const skillName = boundedText(value.skillName, 64);
397
+ const args = typeof value.arguments === "string" && value.arguments.length <= 16384 ? value.arguments : void 0;
398
+ return skillName && args !== void 0 ? { type: "skill", skillName, arguments: args } : null;
399
+ }
400
+ return null;
401
+ }
402
+ function parseStartBody(body, tenant, factoryProjectId) {
403
+ if (!isRecord(body) || !isRecord(body.workItem)) return null;
404
+ const input = parseCreateWorkItem(body.workItem.input);
405
+ const sessionId = boundedText(body.sessionId, 256);
406
+ const threadTitle = boundedText(body.threadTitle, 512);
407
+ const kickoffKey = boundedText(body.kickoffKey, 256);
408
+ const invocation = parseInvocation(body.invocation);
409
+ const destinationStage = FACTORY_RULE_STAGES.includes(body.destinationStage) ? body.destinationStage : void 0;
410
+ const role = boundedText(body.workItem.role, 32);
411
+ const id = body.workItem.id === void 0 ? void 0 : boundedText(body.workItem.id, 64);
412
+ if (body.workItem.id !== void 0 && (!id || !UUID_RE.test(id))) return null;
413
+ if (!input || !sessionId || !UUID_RE.test(sessionId) || !threadTitle || !kickoffKey || !UUID_RE.test(kickoffKey) || invocation === null || !destinationStage || !role) {
414
+ return null;
415
+ }
416
+ const threadTags = isRecord(body.threadTags) ? Object.fromEntries(
417
+ Object.entries(body.threadTags).filter(
418
+ (entry) => boundedText(entry[0], 64) !== void 0 && boundedText(entry[1], 256) !== void 0
419
+ ).map(([key, value]) => [key, value.trim()])
420
+ ) : void 0;
421
+ return {
422
+ ...tenant,
423
+ factoryProjectId,
424
+ sessionId,
425
+ threadTitle,
426
+ threadTags,
427
+ kickoffKey,
428
+ invocation,
429
+ destinationStage,
430
+ workItem: { id, role, input }
431
+ };
432
+ }
433
+ var DECISION_STATUSES = /* @__PURE__ */ new Set(["pending", "leased", "retry", "succeeded", "failed"]);
434
+ var DEFAULT_DECISION_PAGE_SIZE = 25;
435
+ var MAX_DECISION_PAGE_SIZE = 50;
436
+ function parseDecisionStatuses(raw) {
437
+ if (!raw) return void 0;
438
+ const statuses = [...new Set(raw.split(",").map((status) => status.trim()))].filter(
439
+ (status) => DECISION_STATUSES.has(status)
440
+ );
441
+ return statuses.length > 0 ? statuses : void 0;
442
+ }
443
+ function parseDecisionLimit(raw) {
444
+ const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_DECISION_PAGE_SIZE;
445
+ if (!Number.isFinite(parsed)) return DEFAULT_DECISION_PAGE_SIZE;
446
+ return Math.max(1, Math.min(MAX_DECISION_PAGE_SIZE, parsed));
447
+ }
448
+ function encodeDecisionCursor(decision) {
449
+ return Buffer.from(JSON.stringify([decision.createdAt.toISOString(), decision.id]), "utf8").toString("base64url");
450
+ }
451
+ function parseDecisionCursor(raw) {
452
+ if (!raw) return void 0;
453
+ try {
454
+ const decoded = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
455
+ if (!Array.isArray(decoded) || decoded.length !== 2 || typeof decoded[0] !== "string" || typeof decoded[1] !== "string") {
456
+ return void 0;
457
+ }
458
+ const createdAt = new Date(decoded[0]);
459
+ if (Number.isNaN(createdAt.getTime()) || !UUID_RE.test(decoded[1])) return void 0;
460
+ return { createdAt, id: decoded[1] };
461
+ } catch {
462
+ return void 0;
463
+ }
464
+ }
465
+ function decisionSummary(decision) {
466
+ const type = typeof decision.decision.type === "string" ? decision.decision.type.slice(0, 64) : "unknown";
467
+ return {
468
+ id: decision.id,
469
+ evaluationId: decision.evaluationId,
470
+ workItemId: decision.workItemId,
471
+ type,
472
+ status: decision.status,
473
+ attempts: decision.attempts,
474
+ lastError: decision.lastError?.slice(0, 512) ?? null,
475
+ createdAt: decision.createdAt.toISOString(),
476
+ updatedAt: decision.updatedAt.toISOString(),
477
+ completedAt: decision.completedAt?.toISOString() ?? null
478
+ };
479
+ }
480
+ var WorkItemRoutes = class extends Route {
481
+ /** Resolve the `(orgId, userId)` tenant or a ready-to-return error response. */
482
+ async #resolveTenant(c) {
483
+ await this.deps.auth.ensureUser(c);
484
+ const tenant = this.deps.auth.tenant(c);
485
+ if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
486
+ if (!tenant.orgId) {
487
+ return {
488
+ response: c.json(
489
+ { error: "organization_required", message: "The Factory board requires an organization." },
490
+ 403
491
+ )
492
+ };
493
+ }
494
+ return { orgId: tenant.orgId, userId: tenant.userId };
495
+ }
496
+ /**
497
+ * Resolve the tenant AND the org-owned project from the `:id` param. Work
498
+ * items hang off a project, so listing/creating requires the project to
499
+ * exist in the caller's org.
500
+ */
501
+ async #resolveProject(c) {
502
+ const tenant = await this.#resolveTenant(c);
503
+ if ("response" in tenant) return tenant;
504
+ const projectId = c.req.param("id");
505
+ if (!projectId || !UUID_RE.test(projectId)) {
506
+ return { response: c.json({ error: "Project not found" }, 404) };
507
+ }
508
+ const { projects } = this.deps;
509
+ await projects.ensureReady();
510
+ const project = await projects.get({ orgId: tenant.orgId, id: projectId });
511
+ if (!project) {
512
+ return { response: c.json({ error: "Project not found" }, 404) };
513
+ }
514
+ return { ...tenant, factoryProjectId: projectId };
515
+ }
516
+ /**
517
+ * Emit the audit events a successful work-item PATCH implies: always
518
+ * `updated`, plus `stage_moved` when the stages actually changed and one
519
+ * `run.started` per session role the patch introduced.
520
+ */
521
+ async #auditWorkItemPatch({
522
+ context,
523
+ item,
524
+ previous,
525
+ patch
526
+ }) {
527
+ const { audit } = this.deps;
528
+ const target = { type: "work_item", id: item.id, name: item.title };
529
+ await audit.emit({
530
+ context,
531
+ input: {
532
+ action: "factory.work_item.updated",
533
+ factoryProjectId: item.factoryProjectId,
534
+ targets: [target],
535
+ metadata: { fields: patchedFields(patch) }
536
+ }
537
+ });
538
+ const stagesChanged = patch.stages !== void 0 && (previous.stages.length !== item.stages.length || previous.stages.some((s, i) => s !== item.stages[i]));
539
+ if (stagesChanged) {
540
+ await audit.emit({
541
+ context,
542
+ input: {
543
+ action: "factory.work_item.stage_moved",
544
+ factoryProjectId: item.factoryProjectId,
545
+ targets: [target],
546
+ metadata: { from: previous.stages, to: item.stages }
547
+ }
548
+ });
549
+ }
550
+ const newRoles = Object.keys(item.sessions).filter((role) => !previous.sessionRoles.includes(role));
551
+ for (const role of newRoles) {
552
+ const session = item.sessions[role];
553
+ await audit.emit({
554
+ context,
555
+ input: {
556
+ action: "factory.run.started",
557
+ factoryProjectId: item.factoryProjectId,
558
+ targets: [target],
559
+ metadata: {
560
+ role,
561
+ branch: session?.branch,
562
+ threadId: session?.threadId,
563
+ sessionId: session?.sessionId
564
+ }
565
+ }
566
+ });
567
+ }
568
+ }
569
+ /** Build the Factory work-item routes as Mastra `apiRoutes`. */
570
+ routes() {
571
+ const { audit, workItems, queueHealth, transitionService, startCoordinator } = this.deps;
572
+ return [
573
+ // ── List the org's work items for a project ─────────────────────────────
574
+ registerApiRoute("/web/factory/projects/:id/work-items", {
575
+ method: "GET",
576
+ requiresAuth: false,
577
+ handler: async (c) => {
578
+ const resolved = await this.#resolveProject(loose(c));
579
+ if ("response" in resolved) return resolved.response;
580
+ await workItems.ensureReady();
581
+ const items = await workItems.list({
582
+ orgId: resolved.orgId,
583
+ factoryProjectId: resolved.factoryProjectId
584
+ });
585
+ return c.json({ workItems: items });
586
+ }
587
+ }),
588
+ // ── Flow metrics aggregated over the project's work items ───────────────
589
+ registerApiRoute("/web/factory/projects/:id/metrics", {
590
+ method: "GET",
591
+ requiresAuth: false,
592
+ handler: async (c) => {
593
+ const resolved = await this.#resolveProject(loose(c));
594
+ if ("response" in resolved) return resolved.response;
595
+ const { windowStart, windowEnd } = parseMetricsRange(
596
+ loose(c).req.query("from"),
597
+ loose(c).req.query("to"),
598
+ /* @__PURE__ */ new Date()
599
+ );
600
+ await workItems.ensureReady();
601
+ const items = await workItems.list({
602
+ orgId: resolved.orgId,
603
+ factoryProjectId: resolved.factoryProjectId
604
+ });
605
+ return c.json({ metrics: computeFactoryMetrics(items, { windowStart, windowEnd }) });
606
+ }
607
+ }),
608
+ // ── Per-project queue-health age-threshold config (seconds) ─────────────
609
+ registerApiRoute("/web/factory/projects/:id/health/thresholds", {
610
+ method: "GET",
611
+ requiresAuth: false,
612
+ handler: async (c) => {
613
+ const resolved = await this.#resolveProject(loose(c));
614
+ if ("response" in resolved) return resolved.response;
615
+ await queueHealth.ensureReady();
616
+ const stored = await queueHealth.getConfig(resolved.orgId, resolved.factoryProjectId);
617
+ return c.json({ thresholds: thresholdsOrDefault(stored) });
618
+ }
619
+ }),
620
+ // ── Bounded durable rule-decision status ────────────────────────────────
621
+ registerApiRoute("/web/factory/projects/:id/decisions", {
622
+ method: "GET",
623
+ requiresAuth: false,
624
+ handler: async (c) => {
625
+ const context = loose(c);
626
+ const resolved = await this.#resolveProject(context);
627
+ if ("response" in resolved) return resolved.response;
628
+ const cursorRaw = context.req.query("before");
629
+ const before = parseDecisionCursor(cursorRaw);
630
+ if (cursorRaw && !before) return c.json({ error: "invalid_cursor" }, 400);
631
+ await workItems.ensureReady();
632
+ const page = await workItems.listDeferredDecisionPage({
633
+ orgId: resolved.orgId,
634
+ factoryProjectId: resolved.factoryProjectId,
635
+ statuses: parseDecisionStatuses(context.req.query("statuses")),
636
+ before,
637
+ limit: parseDecisionLimit(context.req.query("limit"))
638
+ });
639
+ const last = page.decisions.at(-1);
640
+ return c.json({
641
+ decisions: page.decisions.map(decisionSummary),
642
+ ...page.hasMore && last ? { nextCursor: encodeDecisionCursor(last) } : {}
643
+ });
644
+ }
645
+ }),
646
+ registerApiRoute("/web/factory/projects/:id/decisions/:decisionId/retry", {
647
+ method: "POST",
648
+ requiresAuth: false,
649
+ handler: async (c) => {
650
+ const context = loose(c);
651
+ const resolved = await this.#resolveProject(context);
652
+ if ("response" in resolved) return resolved.response;
653
+ const decisionId = context.req.param("decisionId");
654
+ if (!decisionId || !UUID_RE.test(decisionId)) return c.json({ error: "invalid_decision_id" }, 422);
655
+ await workItems.ensureReady();
656
+ const decision = await workItems.retryDeferredDecision(
657
+ resolved.orgId,
658
+ resolved.factoryProjectId,
659
+ decisionId,
660
+ /* @__PURE__ */ new Date()
661
+ );
662
+ if (!decision) return c.json({ error: "decision_not_retryable" }, 409);
663
+ return c.json({ decision: decisionSummary(decision) });
664
+ }
665
+ }),
666
+ // ── Create (upsert on sourceKey) a work item ─────────────────────────────
667
+ registerApiRoute("/web/factory/projects/:id/work-items", {
668
+ method: "POST",
669
+ requiresAuth: false,
670
+ handler: async (c) => {
671
+ const resolved = await this.#resolveProject(loose(c));
672
+ if ("response" in resolved) return resolved.response;
673
+ const body = await readJson(loose(c));
674
+ if (body === void 0) return c.json({ error: "Invalid JSON body" }, 400);
675
+ const input = parseCreateWorkItem(body);
676
+ if (!input) return c.json({ error: "invalid_work_item" }, 400);
677
+ if ((input.stages ?? ["intake"]).length !== 1 || (input.stages ?? ["intake"])[0] !== "intake") {
678
+ return c.json(
679
+ { error: "governed_transition_required", message: "New work items must enter through Factory intake." },
680
+ 409
681
+ );
682
+ }
683
+ await workItems.ensureReady();
684
+ try {
685
+ const result = await workItems.upsert({
686
+ orgId: resolved.orgId,
687
+ userId: resolved.userId,
688
+ factoryProjectId: resolved.factoryProjectId,
689
+ input,
690
+ reuseMode: "non-stage"
691
+ });
692
+ let item = result.item;
693
+ if (result.created) {
694
+ if (!transitionService) {
695
+ await workItems.delete({ orgId: resolved.orgId, id: item.id });
696
+ return c.json({ error: "factory_transitions_unavailable" }, 503);
697
+ }
698
+ const entered = await transitionService.transition({
699
+ orgId: resolved.orgId,
700
+ factoryProjectId: resolved.factoryProjectId,
701
+ workItemId: item.id,
702
+ board: item.externalSource?.type === "pull-request" ? "review" : "work",
703
+ stage: "intake",
704
+ expectedRevision: item.revision,
705
+ actor: { type: "human", id: resolved.userId },
706
+ ingress: { type: "human", identity: `work-item:${item.id}:initial-entry` },
707
+ cause: "work_item_created",
708
+ initialEntry: true
709
+ });
710
+ if (entered.status === "rejected") {
711
+ await workItems.delete({ orgId: resolved.orgId, id: item.id });
712
+ return c.json({ status: "rejected", code: entered.code, reason: entered.reason }, 422);
713
+ }
714
+ item = await workItems.getForProject(resolved.orgId, resolved.factoryProjectId, item.id) ?? item;
715
+ await audit.emit({
716
+ context: loose(c),
717
+ input: {
718
+ action: "factory.work_item.created",
719
+ factoryProjectId: resolved.factoryProjectId,
720
+ targets: [{ type: "work_item", id: item.id, name: item.title }],
721
+ metadata: { externalSource: item.externalSource, stages: item.stages }
722
+ }
723
+ });
724
+ } else {
725
+ const { stages: _stages, sessions: _sessions, ...boundedPatch } = input;
726
+ await this.#auditWorkItemPatch({
727
+ context: loose(c),
728
+ item,
729
+ previous: result.previous,
730
+ patch: boundedPatch
731
+ });
732
+ }
733
+ return c.json({ workItem: item });
734
+ } catch (error) {
735
+ if (error instanceof WorkItemRelationError) {
736
+ return c.json({ error: error.code, message: error.message }, 400);
737
+ }
738
+ throw error;
739
+ }
740
+ }
741
+ }),
742
+ // ── Authoritative stage transition ──────────────────────────────────────
743
+ registerApiRoute("/web/factory/projects/:id/work-items/:workItemId/transition", {
744
+ method: "POST",
745
+ requiresAuth: false,
746
+ handler: async (c) => {
747
+ const resolved = await this.#resolveProject(loose(c));
748
+ if ("response" in resolved) return resolved.response;
749
+ const workItemId = loose(c).req.param("workItemId");
750
+ if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: "Work item not found" }, 404);
751
+ const parsed = parseTransitionBody(await readJson(loose(c)));
752
+ if (!parsed) return c.json({ error: "invalid_transition_request" }, 400);
753
+ if (!transitionService) {
754
+ return c.json({ error: "factory_transition_unavailable" }, 503);
755
+ }
756
+ await workItems.ensureReady();
757
+ const result = await transitionService.transition({
758
+ ...parsed,
759
+ orgId: resolved.orgId,
760
+ factoryProjectId: resolved.factoryProjectId,
761
+ workItemId,
762
+ actor: { type: "human", id: resolved.userId },
763
+ ingress: {
764
+ ...parsed.ingress,
765
+ identity: `human:${resolved.userId}:${parsed.ingress.identity}`
766
+ }
767
+ });
768
+ await audit.emit({
769
+ context: loose(c),
770
+ input: {
771
+ action: result.status === "accepted" ? "factory.work_item.stage_moved" : "factory.work_item.transition_rejected",
772
+ factoryProjectId: resolved.factoryProjectId,
773
+ targets: [{ type: "work_item", id: workItemId }],
774
+ metadata: {
775
+ transitionId: result.transitionId,
776
+ ingressType: parsed.ingress.type,
777
+ ruleSetVersion: transitionService.ruleSetVersion,
778
+ ...result.status === "accepted" ? { to: result.stage, revision: result.revision } : { code: result.code, reason: result.reason }
779
+ }
780
+ }
781
+ });
782
+ if (result.status === "accepted") return c.json({ result });
783
+ return c.json({ result }, result.code === "stale" ? 409 : 422);
784
+ }
785
+ }),
786
+ // ── Bind a Factory run before dispatching its kickoff ────────────────────
787
+ registerApiRoute("/web/factory/projects/:id/runs/start", {
788
+ method: "POST",
789
+ requiresAuth: false,
790
+ handler: async (c) => {
791
+ const resolved = await this.#resolveProject(loose(c));
792
+ if ("response" in resolved) return resolved.response;
793
+ if (!startCoordinator) {
794
+ return c.json({ error: "factory_start_unavailable" }, 503);
795
+ }
796
+ const input = parseStartBody(await readJson(loose(c)), resolved, resolved.factoryProjectId);
797
+ if (!input) return c.json({ error: "invalid_factory_start" }, 400);
798
+ input.requestContext = loose(c).get("requestContext");
799
+ if (!input.workItem.id && ((input.workItem.input.stages ?? ["intake"]).length !== 1 || (input.workItem.input.stages ?? ["intake"])[0] !== "intake")) {
800
+ return c.json(
801
+ { error: "governed_transition_required", message: "Create the work item in Intake before starting it." },
802
+ 409
803
+ );
804
+ }
805
+ await workItems.ensureReady();
806
+ let prepared;
807
+ try {
808
+ prepared = await startCoordinator.prepare(input);
809
+ } catch (error) {
810
+ if (error instanceof FactoryStartTransitionError) {
811
+ return c.json({ result: error.result }, error.result.code === "stale" ? 409 : 422);
812
+ }
813
+ throw error;
814
+ }
815
+ await audit.emit({
816
+ context: loose(c),
817
+ input: {
818
+ action: "factory.run.started",
819
+ factoryProjectId: resolved.factoryProjectId,
820
+ targets: [{ type: "work_item", id: prepared.workItemId }],
821
+ metadata: {
822
+ role: input.workItem.role,
823
+ branch: prepared.branch,
824
+ threadId: prepared.threadId,
825
+ sessionId: prepared.sessionId,
826
+ bindingId: prepared.bindingId
827
+ }
828
+ }
829
+ });
830
+ return c.json({ prepared }, 202);
831
+ }
832
+ }),
833
+ // ── Patch non-stage metadata / sessions / title ──────────────────────────
834
+ registerApiRoute("/web/factory/work-items/:id", {
835
+ method: "PATCH",
836
+ requiresAuth: false,
837
+ handler: async (c) => {
838
+ const tenant = await this.#resolveTenant(loose(c));
839
+ if ("response" in tenant) return tenant.response;
840
+ const id = loose(c).req.param("id");
841
+ if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
842
+ const body = await readJson(loose(c));
843
+ if (body === void 0) return c.json({ error: "Invalid JSON body" }, 400);
844
+ const patch = parseUpdateWorkItem(body);
845
+ if (!patch) return c.json({ error: "invalid_work_item_patch" }, 400);
846
+ if (patch.stages !== void 0) {
847
+ return c.json(
848
+ { error: "governed_transition_required", message: "Use the Factory transition endpoint to move stages." },
849
+ 409
850
+ );
851
+ }
852
+ await workItems.ensureReady();
853
+ try {
854
+ const updated = await workItems.update({ orgId: tenant.orgId, id, userId: tenant.userId, patch });
855
+ if (!updated) return c.json({ error: "Work item not found" }, 404);
856
+ await this.#auditWorkItemPatch({
857
+ context: loose(c),
858
+ item: updated.item,
859
+ previous: updated.previous,
860
+ patch
861
+ });
862
+ return c.json({ workItem: updated.item });
863
+ } catch (error) {
864
+ if (error instanceof WorkItemRelationError) {
865
+ return c.json({ error: error.code, message: error.message }, 400);
866
+ }
867
+ throw error;
868
+ }
869
+ }
870
+ }),
871
+ // ── Remove a work item ───────────────────────────────────────────────────
872
+ registerApiRoute("/web/factory/work-items/:id", {
873
+ method: "DELETE",
874
+ requiresAuth: false,
875
+ handler: async (c) => {
876
+ const tenant = await this.#resolveTenant(loose(c));
877
+ if ("response" in tenant) return tenant.response;
878
+ const id = loose(c).req.param("id");
879
+ if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
880
+ await workItems.ensureReady();
881
+ const deleted = await workItems.delete({ orgId: tenant.orgId, id });
882
+ if (!deleted) return c.json({ error: "Work item not found" }, 404);
883
+ await audit.emit({
884
+ context: loose(c),
885
+ input: {
886
+ action: "factory.work_item.deleted",
887
+ factoryProjectId: deleted.factoryProjectId,
888
+ targets: [{ type: "work_item", id: deleted.id, name: deleted.title }]
889
+ }
890
+ });
891
+ return c.json({ ok: true });
892
+ }
893
+ })
894
+ ];
895
+ }
896
+ };
897
+ export {
898
+ WorkItemRoutes,
899
+ parseCreateWorkItem,
900
+ parseUpdateWorkItem
901
+ };
902
+ //# sourceMappingURL=work-items.js.map