@mastra/factory 0.13.0-alpha.9 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +232 -10
  2. package/dist/auth.d.ts +2 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +29 -4
  5. package/dist/auth.js.map +1 -1
  6. package/dist/boards/define-board.d.ts +36 -2
  7. package/dist/boards/define-board.d.ts.map +1 -1
  8. package/dist/boards/define-board.js +58 -1
  9. package/dist/boards/define-board.js.map +1 -1
  10. package/dist/boards/index.d.ts +4 -2
  11. package/dist/boards/index.d.ts.map +1 -1
  12. package/dist/boards/index.js +2 -1
  13. package/dist/boards/index.js.map +1 -1
  14. package/dist/boards/relocate.d.ts +17 -0
  15. package/dist/boards/relocate.d.ts.map +1 -0
  16. package/dist/boards/relocate.js +53 -0
  17. package/dist/boards/relocate.js.map +1 -0
  18. package/dist/boards/review.d.ts +1 -1
  19. package/dist/boards/review.d.ts.map +1 -1
  20. package/dist/boards/review.js +17 -4
  21. package/dist/boards/review.js.map +1 -1
  22. package/dist/boards/semantics.d.ts +28 -0
  23. package/dist/boards/semantics.d.ts.map +1 -0
  24. package/dist/boards/semantics.js +41 -0
  25. package/dist/boards/semantics.js.map +1 -0
  26. package/dist/boards/work-tool-rules.d.ts +8 -0
  27. package/dist/boards/work-tool-rules.d.ts.map +1 -0
  28. package/dist/boards/work-tool-rules.js +20 -0
  29. package/dist/boards/work-tool-rules.js.map +1 -0
  30. package/dist/boards/work.d.ts +1 -1
  31. package/dist/boards/work.d.ts.map +1 -1
  32. package/dist/boards/work.js +13 -0
  33. package/dist/boards/work.js.map +1 -1
  34. package/dist/capabilities/intake.d.ts +2 -0
  35. package/dist/capabilities/intake.d.ts.map +1 -1
  36. package/dist/factory.d.ts +5 -6
  37. package/dist/factory.d.ts.map +1 -1
  38. package/dist/factory.js +41 -12
  39. package/dist/factory.js.map +1 -1
  40. package/dist/integrations/base.d.ts +7 -4
  41. package/dist/integrations/base.d.ts.map +1 -1
  42. package/dist/integrations/github/default-rules.d.ts +2 -2
  43. package/dist/integrations/github/default-rules.d.ts.map +1 -1
  44. package/dist/integrations/github/default-rules.js +2 -2
  45. package/dist/integrations/github/default-rules.js.map +1 -1
  46. package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
  47. package/dist/integrations/github/issue-reconciler.js +4 -3
  48. package/dist/integrations/github/issue-reconciler.js.map +1 -1
  49. package/dist/integrations/github/rules.d.ts +6 -2
  50. package/dist/integrations/github/rules.d.ts.map +1 -1
  51. package/dist/integrations/github/rules.js +96 -15
  52. package/dist/integrations/github/rules.js.map +1 -1
  53. package/dist/integrations/github/sandbox.d.ts +6 -3
  54. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  55. package/dist/integrations/github/sandbox.js +21 -2
  56. package/dist/integrations/github/sandbox.js.map +1 -1
  57. package/dist/integrations/issue-reconciler.d.ts +2 -0
  58. package/dist/integrations/issue-reconciler.d.ts.map +1 -1
  59. package/dist/integrations/issue-reconciler.js +1 -3
  60. package/dist/integrations/issue-reconciler.js.map +1 -1
  61. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  62. package/dist/integrations/linear/agent-tools.js.map +1 -1
  63. package/dist/integrations/linear/default-rules.d.ts +2 -2
  64. package/dist/integrations/linear/default-rules.d.ts.map +1 -1
  65. package/dist/integrations/linear/default-rules.js +2 -2
  66. package/dist/integrations/linear/default-rules.js.map +1 -1
  67. package/dist/integrations/linear/integration.js +1 -0
  68. package/dist/integrations/linear/integration.js.map +1 -1
  69. package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
  70. package/dist/integrations/linear/issue-reconciler.js +9 -4
  71. package/dist/integrations/linear/issue-reconciler.js.map +1 -1
  72. package/dist/integrations/linear/reconciliation-config.d.ts.map +1 -1
  73. package/dist/integrations/linear/reconciliation-config.js.map +1 -1
  74. package/dist/integrations/linear/routes.d.ts.map +1 -1
  75. package/dist/integrations/linear/routes.js +19 -26
  76. package/dist/integrations/linear/routes.js.map +1 -1
  77. package/dist/integrations/linear/rules.d.ts +7 -2
  78. package/dist/integrations/linear/rules.d.ts.map +1 -1
  79. package/dist/integrations/linear/rules.js +20 -8
  80. package/dist/integrations/linear/rules.js.map +1 -1
  81. package/dist/integrations/platform/api-client.d.ts.map +1 -1
  82. package/dist/integrations/platform/api-client.js +25 -4
  83. package/dist/integrations/platform/api-client.js.map +1 -1
  84. package/dist/integrations/platform/github/integration.d.ts +1 -0
  85. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  86. package/dist/integrations/platform/github/integration.js +5 -0
  87. package/dist/integrations/platform/github/integration.js.map +1 -1
  88. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  89. package/dist/integrations/platform/linear/integration.js +5 -2
  90. package/dist/integrations/platform/linear/integration.js.map +1 -1
  91. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts +8 -0
  92. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts.map +1 -0
  93. package/dist/integrations/slack/emoji-shortcodes.generated.js +1985 -0
  94. package/dist/integrations/slack/emoji-shortcodes.generated.js.map +1 -0
  95. package/dist/integrations/slack/emoji.d.ts +3 -0
  96. package/dist/integrations/slack/emoji.d.ts.map +1 -0
  97. package/dist/integrations/slack/emoji.js +12 -0
  98. package/dist/integrations/slack/emoji.js.map +1 -0
  99. package/dist/integrations/slack/integration.js +1 -1
  100. package/dist/integrations/slack/integration.js.map +1 -1
  101. package/dist/integrations/slack/slack.d.ts.map +1 -1
  102. package/dist/integrations/slack/slack.js +16 -2
  103. package/dist/integrations/slack/slack.js.map +1 -1
  104. package/dist/routes/attention-activity.d.ts +2 -3
  105. package/dist/routes/attention-activity.d.ts.map +1 -1
  106. package/dist/routes/attention-activity.js +12 -3
  107. package/dist/routes/attention-activity.js.map +1 -1
  108. package/dist/routes/attention-parked.d.ts +22 -0
  109. package/dist/routes/attention-parked.d.ts.map +1 -0
  110. package/dist/routes/attention-parked.js +136 -0
  111. package/dist/routes/attention-parked.js.map +1 -0
  112. package/dist/routes/attention-providers.js +8 -8
  113. package/dist/routes/attention-providers.js.map +1 -1
  114. package/dist/routes/attention.d.ts +2 -0
  115. package/dist/routes/attention.d.ts.map +1 -1
  116. package/dist/routes/attention.js +47 -118
  117. package/dist/routes/attention.js.map +1 -1
  118. package/dist/routes/contracts.d.ts +627 -0
  119. package/dist/routes/contracts.d.ts.map +1 -0
  120. package/dist/routes/contracts.js +468 -0
  121. package/dist/routes/contracts.js.map +1 -0
  122. package/dist/routes/intake.d.ts +18 -0
  123. package/dist/routes/intake.d.ts.map +1 -1
  124. package/dist/routes/intake.js +265 -15
  125. package/dist/routes/intake.js.map +1 -1
  126. package/dist/routes/projects.d.ts.map +1 -1
  127. package/dist/routes/projects.js +26 -76
  128. package/dist/routes/projects.js.map +1 -1
  129. package/dist/routes/supervisor.d.ts +2 -0
  130. package/dist/routes/supervisor.d.ts.map +1 -1
  131. package/dist/routes/supervisor.js +7 -6
  132. package/dist/routes/supervisor.js.map +1 -1
  133. package/dist/routes/surface.d.ts +7 -4
  134. package/dist/routes/surface.d.ts.map +1 -1
  135. package/dist/routes/surface.js +17 -11
  136. package/dist/routes/surface.js.map +1 -1
  137. package/dist/routes/telemetry.d.ts +13 -0
  138. package/dist/routes/telemetry.d.ts.map +1 -0
  139. package/dist/routes/telemetry.js +60 -0
  140. package/dist/routes/telemetry.js.map +1 -0
  141. package/dist/routes/work-items.d.ts +3 -3
  142. package/dist/routes/work-items.d.ts.map +1 -1
  143. package/dist/routes/work-items.js +134 -250
  144. package/dist/routes/work-items.js.map +1 -1
  145. package/dist/rules/dispatch-errors.d.ts +2 -2
  146. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  147. package/dist/rules/dispatch-errors.js +10 -6
  148. package/dist/rules/dispatch-errors.js.map +1 -1
  149. package/dist/rules/dispatcher.d.ts +9 -2
  150. package/dist/rules/dispatcher.d.ts.map +1 -1
  151. package/dist/rules/dispatcher.js +76 -23
  152. package/dist/rules/dispatcher.js.map +1 -1
  153. package/dist/rules/index.d.ts +4 -5
  154. package/dist/rules/index.d.ts.map +1 -1
  155. package/dist/rules/index.js +3 -4
  156. package/dist/rules/processor.d.ts +3 -3
  157. package/dist/rules/processor.d.ts.map +1 -1
  158. package/dist/rules/processor.js +24 -31
  159. package/dist/rules/processor.js.map +1 -1
  160. package/dist/rules/resolve.d.ts +1 -3
  161. package/dist/rules/resolve.d.ts.map +1 -1
  162. package/dist/rules/resolve.js +1 -4
  163. package/dist/rules/resolve.js.map +1 -1
  164. package/dist/rules/start-coordinator.d.ts.map +1 -1
  165. package/dist/rules/start-coordinator.js +3 -1
  166. package/dist/rules/start-coordinator.js.map +1 -1
  167. package/dist/rules/tools.d.ts.map +1 -1
  168. package/dist/rules/tools.js +20 -10
  169. package/dist/rules/tools.js.map +1 -1
  170. package/dist/rules/transition-service.d.ts +7 -7
  171. package/dist/rules/transition-service.d.ts.map +1 -1
  172. package/dist/rules/transition-service.js +21 -25
  173. package/dist/rules/transition-service.js.map +1 -1
  174. package/dist/rules/types.d.ts +16 -11
  175. package/dist/rules/types.d.ts.map +1 -1
  176. package/dist/rules/types.js.map +1 -1
  177. package/dist/rules/validation.d.ts +12 -2
  178. package/dist/rules/validation.d.ts.map +1 -1
  179. package/dist/rules/validation.js +29 -20
  180. package/dist/rules/validation.js.map +1 -1
  181. package/dist/session/live-sessions.d.ts +33 -3
  182. package/dist/session/live-sessions.d.ts.map +1 -1
  183. package/dist/session/live-sessions.js +67 -5
  184. package/dist/session/live-sessions.js.map +1 -1
  185. package/dist/storage/domains/intake/base.d.ts +54 -1
  186. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  187. package/dist/storage/domains/intake/base.js +136 -4
  188. package/dist/storage/domains/intake/base.js.map +1 -1
  189. package/dist/storage/domains/work-items/base.d.ts +22 -7
  190. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  191. package/dist/storage/domains/work-items/base.js +41 -26
  192. package/dist/storage/domains/work-items/base.js.map +1 -1
  193. package/dist/supervisor/health-worker.d.ts +2 -0
  194. package/dist/supervisor/health-worker.d.ts.map +1 -1
  195. package/dist/supervisor/health-worker.js +3 -1
  196. package/dist/supervisor/health-worker.js.map +1 -1
  197. package/dist/supervisor/health.d.ts +12 -14
  198. package/dist/supervisor/health.d.ts.map +1 -1
  199. package/dist/supervisor/health.js +24 -52
  200. package/dist/supervisor/health.js.map +1 -1
  201. package/dist/supervisor/instructions.d.ts +1 -1
  202. package/dist/supervisor/instructions.d.ts.map +1 -1
  203. package/dist/supervisor/instructions.js +10 -8
  204. package/dist/supervisor/instructions.js.map +1 -1
  205. package/dist/supervisor/read-tools.d.ts +2 -0
  206. package/dist/supervisor/read-tools.d.ts.map +1 -1
  207. package/dist/supervisor/read-tools.js +2 -2
  208. package/dist/supervisor/read-tools.js.map +1 -1
  209. package/dist/supervisor/write-tools.d.ts.map +1 -1
  210. package/dist/supervisor/write-tools.js +6 -4
  211. package/dist/supervisor/write-tools.js.map +1 -1
  212. package/dist/telemetry-types.d.ts +7 -0
  213. package/dist/telemetry-types.d.ts.map +1 -0
  214. package/dist/telemetry-types.js +24 -0
  215. package/dist/telemetry-types.js.map +1 -0
  216. package/dist/telemetry.d.ts +9 -0
  217. package/dist/telemetry.d.ts.map +1 -0
  218. package/dist/telemetry.js +47 -0
  219. package/dist/telemetry.js.map +1 -0
  220. package/dist/work-item-branch.d.ts +2 -0
  221. package/dist/work-item-branch.d.ts.map +1 -1
  222. package/dist/work-item-branch.js +6 -1
  223. package/dist/work-item-branch.js.map +1 -1
  224. package/dist/workspace.d.ts.map +1 -1
  225. package/dist/workspace.js +3 -1
  226. package/dist/workspace.js.map +1 -1
  227. package/factory-skills/configure-factory-rules/SKILL.md +30 -10
  228. package/package.json +12 -10
  229. package/dist/rules/defaults.d.ts +0 -9
  230. package/dist/rules/defaults.d.ts.map +0 -1
  231. package/dist/rules/defaults.js +0 -50
  232. package/dist/rules/defaults.js.map +0 -1
@@ -1,11 +1,10 @@
1
- import { isFactoryRuleStage } from "../rules/types.js";
2
1
  import { createBoardRegistry } from "../boards/registry.js";
3
2
  import "../boards/index.js";
4
3
  import { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, FACTORY_RULE_MATERIALIZATION_KEY, WorkItemRelationError, WorkItemUpdateConflictError } from "../storage/domains/work-items/base.js";
4
+ import { FACTORY_ROUTE_CONTRACTS } from "./contracts.js";
5
5
  import { Route } from "./route.js";
6
6
  import { factoryDispatchFailureMetadata } from "../rules/dispatch-errors.js";
7
7
  import { FactoryStartTransitionError } from "../rules/start-coordinator.js";
8
- import { roleForStage } from "../rules/transition-service.js";
9
8
  import { thresholdsOrDefault } from "../storage/domains/queue-health/base.js";
10
9
  import { computeFactoryMetrics, parseMetricsRange } from "../storage/domains/work-items/metrics.js";
11
10
  import { factoryDecisionType } from "./attention-providers.js";
@@ -21,133 +20,29 @@ function toWireWorkItem(item) {
21
20
  metadata: publicWorkItemMetadata(item.metadata) ?? {}
22
21
  };
23
22
  }
24
- /** Session ids of the listed cards whose agent run is in flight. */
25
- function runningSessionIds(items, liveSessions) {
26
- const running = /* @__PURE__ */ new Set();
27
- for (const item of items) for (const { sessionId } of Object.values(item.sessions)) if (liveSessions.isRunning(sessionId)) running.add(sessionId);
28
- return [...running];
23
+ /** Session ids of the listed cards that the predicate picks, each once. */
24
+ function sessionIdsWhere(items, predicate) {
25
+ const picked = /* @__PURE__ */ new Set();
26
+ for (const item of items) for (const { sessionId } of Object.values(item.sessions)) if (predicate(sessionId)) picked.add(sessionId);
27
+ return [...picked];
29
28
  }
30
29
  function loose(c) {
31
30
  return c;
32
31
  }
33
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
34
- const MAX_STAGES = 16;
35
- const MAX_STAGE_LENGTH = 64;
36
- const MAX_METADATA_BYTES = 16 * 1024;
37
- function isRecord(value) {
38
- return typeof value === "object" && value !== null && !Array.isArray(value);
39
- }
40
- function validStages(value) {
41
- return Array.isArray(value) && value.length > 0 && value.length <= MAX_STAGES && value.every((stage) => typeof stage === "string" && stage.length <= MAX_STAGE_LENGTH && /^[a-z0-9][a-z0-9_-]*$/i.test(stage)) && new Set(value).size === value.length;
42
- }
43
- function validMetadata(value) {
44
- if (value === null) return true;
45
- if (!isRecord(value)) return false;
46
- try {
47
- return JSON.stringify(value).length <= MAX_METADATA_BYTES;
48
- } catch {
49
- return false;
50
- }
51
- }
52
32
  function publicWorkItemMetadata(value) {
53
33
  if (value === null) return null;
54
34
  const { [FACTORY_RULE_MATERIALIZATION_KEY]: _materialization, [FACTORY_PULL_REQUEST_RECONCILIATION_KEY]: _reconciliation, ...metadata } = value;
55
35
  return metadata;
56
36
  }
57
- function parseExternalSource(value) {
58
- if (value === void 0 || value === null) return value;
59
- if (!isRecord(value)) return void 0;
60
- const { integrationId, type, externalId, url } = value;
61
- if (typeof integrationId !== "string" || integrationId.length === 0 || integrationId.length > 128) return void 0;
62
- if (typeof type !== "string" || type.length === 0 || type.length > 128) return void 0;
63
- if (typeof externalId !== "string" || externalId.length === 0 || externalId.length > 512) return void 0;
64
- if (url !== void 0 && (typeof url !== "string" || url.length > 2048)) return void 0;
65
- return {
66
- integrationId,
67
- type,
68
- externalId,
69
- ...url !== void 0 ? { url } : {}
70
- };
71
- }
72
- function parseParentWorkItemId(value) {
73
- if (value === null) return null;
74
- if (typeof value !== "string" || !UUID_RE.test(value)) return void 0;
75
- return value;
76
- }
77
- function parseSessions(value) {
78
- if (!isRecord(value)) return void 0;
79
- const out = {};
80
- for (const [role, session] of Object.entries(value)) {
81
- if (!role || role.length > 64 || !isRecord(session)) return void 0;
82
- const { sessionId, branch, threadId } = session;
83
- if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 512) return void 0;
84
- if (typeof branch !== "string" || branch.length === 0 || branch.length > 512) return void 0;
85
- if (typeof threadId !== "string" || threadId.length === 0 || threadId.length > 512) return void 0;
86
- out[role] = {
87
- sessionId,
88
- branch,
89
- threadId
90
- };
91
- }
92
- return out;
93
- }
94
37
  /** Validate an untrusted create body. Unknown keys are dropped. */
95
38
  function parseCreateWorkItem(body) {
96
- if (!isRecord(body)) return null;
97
- const { board, externalSource, title, stages, sessions, metadata } = body;
98
- if (typeof title !== "string" || title.trim().length === 0 || title.length > 500) return null;
99
- if (board !== void 0 && (typeof board !== "string" || board.length === 0 || board.length > 128)) return null;
100
- const hasParentWorkItemId = "parentWorkItemId" in body;
101
- const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
102
- if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
103
- const parsedSource = parseExternalSource(externalSource);
104
- if (externalSource !== void 0 && parsedSource === void 0) return null;
105
- if (stages !== void 0 && !validStages(stages)) return null;
106
- const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
107
- if (sessions !== void 0 && parsedSessions === void 0) return null;
108
- let parsedMetadata;
109
- if (metadata !== void 0) {
110
- if (!validMetadata(metadata)) return null;
111
- parsedMetadata = publicWorkItemMetadata(metadata);
112
- }
113
- return {
114
- title: title.trim(),
115
- ...board !== void 0 ? { board } : {},
116
- ...parsedSource !== void 0 ? { externalSource: parsedSource } : {},
117
- ...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
118
- ...stages !== void 0 ? { stages } : {},
119
- ...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
120
- ...parsedMetadata !== void 0 ? { metadata: parsedMetadata } : {}
121
- };
39
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemCreate.bodySchema.safeParse(body);
40
+ return parsed.success ? parsed.data : null;
122
41
  }
123
42
  /** Validate an untrusted patch body. Unknown keys are dropped. */
124
43
  function parseUpdateWorkItem(body) {
125
- if (!isRecord(body)) return null;
126
- const { board, title, stages, sessions, metadata, plansPreapproved } = body;
127
- const hasParentWorkItemId = "parentWorkItemId" in body;
128
- if (board === void 0 && title === void 0 && stages === void 0 && sessions === void 0 && metadata === void 0 && plansPreapproved === void 0 && !hasParentWorkItemId) return null;
129
- if (board !== void 0 && (typeof board !== "string" || board.length === 0 || board.length > 128)) return null;
130
- if (plansPreapproved !== void 0 && plansPreapproved !== true) return null;
131
- const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
132
- if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
133
- if (title !== void 0 && (typeof title !== "string" || title.trim().length === 0 || title.length > 500)) return null;
134
- if (stages !== void 0 && !validStages(stages)) return null;
135
- const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
136
- if (sessions !== void 0 && parsedSessions === void 0) return null;
137
- let parsedMetadata;
138
- if (metadata !== void 0) {
139
- if (!validMetadata(metadata)) return null;
140
- parsedMetadata = publicWorkItemMetadata(metadata);
141
- }
142
- return {
143
- ...board !== void 0 ? { board } : {},
144
- ...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
145
- ...title !== void 0 ? { title: title.trim() } : {},
146
- ...stages !== void 0 ? { stages } : {},
147
- ...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
148
- ...parsedMetadata !== void 0 ? { metadata: parsedMetadata } : {},
149
- ...plansPreapproved === true ? { plansPreapproved: true } : {}
150
- };
44
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemUpdate.bodySchema.safeParse(body);
45
+ return parsed.success ? parsed.data : null;
151
46
  }
152
47
  async function readJson(c) {
153
48
  try {
@@ -160,100 +55,28 @@ async function readJson(c) {
160
55
  function patchedFields(patch) {
161
56
  return Object.keys(patch).filter((key) => patch[key] !== void 0);
162
57
  }
163
- function boundedText(value, max) {
164
- if (typeof value !== "string") return void 0;
165
- const normalized = value.trim();
166
- return normalized.length > 0 && normalized.length <= max ? normalized : void 0;
167
- }
168
58
  function parseTransitionBody(body) {
169
- if (!isRecord(body)) return null;
170
- const board = typeof body.board === "string" && body.board.length > 0 ? body.board : void 0;
171
- const stage = typeof body.stage === "string" && body.stage.length > 0 ? body.stage : void 0;
172
- const requestId = boundedText(body.requestId, 256);
173
- const cause = boundedText(body.cause, 256);
174
- if (!board || !stage || !requestId || !UUID_RE.test(requestId) || !cause || !Number.isInteger(body.expectedRevision) || Number(body.expectedRevision) < 1) return null;
175
- return {
176
- board,
177
- stage,
178
- expectedRevision: Number(body.expectedRevision),
179
- ingress: {
180
- type: "human",
181
- identity: requestId
182
- },
183
- cause,
184
- ...body.reenter === true ? { reenter: true } : {}
185
- };
59
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemTransition.bodySchema.safeParse(body);
60
+ return parsed.success ? parsed.data : null;
186
61
  }
187
62
  function parseStartBody(body, tenant, factoryProjectId) {
188
- if (!isRecord(body) || !isRecord(body.workItem)) return null;
189
- const input = parseCreateWorkItem(body.workItem.input);
190
- const sessionId = boundedText(body.sessionId, 256);
191
- const threadTitle = boundedText(body.threadTitle, 512);
192
- const kickoffKey = boundedText(body.kickoffKey, 256);
193
- const role = boundedText(body.workItem.role, 32);
194
- const id = boundedText(body.workItem.id, 64);
195
- if (!input || !sessionId || !UUID_RE.test(sessionId) || !threadTitle || !kickoffKey || !UUID_RE.test(kickoffKey) || !id || !UUID_RE.test(id) || !role) return null;
196
- return {
63
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemStart.bodySchema.safeParse(body);
64
+ return parsed.success ? {
197
65
  ...tenant,
198
66
  factoryProjectId,
199
- sessionId,
200
- threadTitle,
201
- kickoffKey,
202
- workItem: {
203
- id,
204
- role,
205
- input
206
- }
207
- };
208
- }
209
- const DECISION_STATUSES = /* @__PURE__ */ new Set([
210
- "pending",
211
- "proposed",
212
- "dismissed",
213
- "superseded",
214
- "leased",
215
- "retry",
216
- "succeeded",
217
- "failed"
218
- ]);
219
- const DEFAULT_DECISION_PAGE_SIZE = 25;
220
- const MAX_DECISION_PAGE_SIZE = 50;
221
- function parseDecisionStatuses(raw) {
222
- if (!raw) return void 0;
223
- const statuses = [...new Set(raw.split(",").map((status) => status.trim()))].filter((status) => DECISION_STATUSES.has(status));
224
- return statuses.length > 0 ? statuses : void 0;
225
- }
226
- function parseDecisionLimit(raw) {
227
- const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_DECISION_PAGE_SIZE;
228
- if (!Number.isFinite(parsed)) return DEFAULT_DECISION_PAGE_SIZE;
229
- return Math.max(1, Math.min(MAX_DECISION_PAGE_SIZE, parsed));
67
+ ...parsed.data
68
+ } : null;
230
69
  }
231
70
  function encodeDecisionCursor(decision) {
232
71
  return Buffer.from(JSON.stringify([decision.createdAt.toISOString(), decision.id]), "utf8").toString("base64url");
233
72
  }
234
- function parseDecisionCursor(raw) {
235
- if (!raw) return void 0;
236
- try {
237
- const decoded = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
238
- if (!Array.isArray(decoded) || decoded.length !== 2 || typeof decoded[0] !== "string" || typeof decoded[1] !== "string") return;
239
- const createdAt = new Date(decoded[0]);
240
- if (Number.isNaN(createdAt.getTime()) || !UUID_RE.test(decoded[1])) return void 0;
241
- return {
242
- createdAt,
243
- id: decoded[1]
244
- };
245
- } catch {
246
- return;
247
- }
248
- }
249
73
  /** A proposed transition names the seat its lane addresses, so the card can label what approving starts. */
250
- function summaryRole(decision) {
74
+ function summaryRole(boards, decision) {
251
75
  if (typeof decision.role === "string") return decision.role.slice(0, 32);
252
76
  if (decision.type !== "transition") return null;
253
- const board = decision.board;
254
- const stage = decision.stage;
255
- if (board !== "work" && board !== "review" || !isFactoryRuleStage(stage)) return null;
256
- return roleForStage(board, stage);
77
+ const { board, stage } = decision;
78
+ if (typeof board !== "string" || typeof stage !== "string") return null;
79
+ return boards.get(board)?.roleForPhase(stage) ?? null;
257
80
  }
258
81
  /** A linked-card decision names where the card is synced from, so the UI can say "GitHub" rather than "a linked card". */
259
82
  function summarySource(decision) {
@@ -261,13 +84,13 @@ function summarySource(decision) {
261
84
  const source = decision.source;
262
85
  return source === "github-issue" || source === "github-pr" || source === "linear-issue" || source === "manual" ? source : null;
263
86
  }
264
- function decisionSummary(decision) {
87
+ function decisionSummary(boards, decision) {
265
88
  return {
266
89
  id: decision.id,
267
90
  evaluationId: decision.evaluationId,
268
91
  workItemId: decision.workItemId,
269
92
  type: factoryDecisionType(decision),
270
- role: summaryRole(decision.decision),
93
+ role: summaryRole(boards, decision.decision),
271
94
  source: summarySource(decision.decision),
272
95
  status: decision.status,
273
96
  attempts: decision.attempts,
@@ -281,6 +104,10 @@ function decisionSummary(decision) {
281
104
  };
282
105
  }
283
106
  var WorkItemRoutes = class extends Route {
107
+ #defaultBoards;
108
+ get #boards() {
109
+ return this.deps.boardRegistry ?? (this.#defaultBoards ??= createBoardRegistry());
110
+ }
284
111
  /** Resolve the `(orgId, userId)` tenant or a ready-to-return error response. */
285
112
  async #resolveTenant(c) {
286
113
  await this.deps.auth.ensureUser(c);
@@ -303,8 +130,9 @@ var WorkItemRoutes = class extends Route {
303
130
  async #resolveProject(c) {
304
131
  const tenant = await this.#resolveTenant(c);
305
132
  if ("response" in tenant) return tenant;
306
- const projectId = c.req.param("id");
307
- if (!projectId || !UUID_RE.test(projectId)) return { response: c.json({ error: "Project not found" }, 404) };
133
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.projectGet.pathSchema.safeParse({ id: c.req.param("id") });
134
+ if (!parsedPath.success) return { response: c.json({ error: "Project not found" }, 404) };
135
+ const projectId = parsedPath.data.id;
308
136
  const { projects } = this.deps;
309
137
  await projects.ensureReady();
310
138
  const project = await projects.get({
@@ -373,15 +201,20 @@ var WorkItemRoutes = class extends Route {
373
201
  /** Releasing a parked run and dropping it: same request, opposite outcomes, both audited as consent. */
374
202
  #proposalRoute({ verb, settle }) {
375
203
  const { audit, workItems } = this.deps;
376
- return registerApiRoute(`/web/factory/projects/:id/decisions/:decisionId/${verb}`, {
377
- method: "POST",
204
+ const contract = verb === "approve" ? FACTORY_ROUTE_CONTRACTS.decisionApprove : FACTORY_ROUTE_CONTRACTS.decisionDismiss;
205
+ return registerApiRoute(contract.path, {
206
+ method: contract.method,
378
207
  requiresAuth: false,
379
208
  handler: async (c) => {
380
209
  const context = loose(c);
381
210
  const resolved = await this.#resolveProject(context);
382
211
  if ("response" in resolved) return resolved.response;
383
- const decisionId = context.req.param("decisionId");
384
- if (!decisionId || !UUID_RE.test(decisionId)) return c.json({ error: "invalid_decision_id" }, 422);
212
+ const parsedPath = contract.pathSchema.safeParse({
213
+ id: resolved.factoryProjectId,
214
+ decisionId: context.req.param("decisionId")
215
+ });
216
+ if (!parsedPath.success) return c.json({ error: "invalid_decision_id" }, 422);
217
+ const { decisionId } = parsedPath.data;
385
218
  await workItems.ensureReady();
386
219
  const now = /* @__PURE__ */ new Date();
387
220
  const decision = await settle(resolved.orgId, resolved.factoryProjectId, decisionId, now, resolved.userId);
@@ -404,7 +237,7 @@ var WorkItemRoutes = class extends Route {
404
237
  }
405
238
  }
406
239
  });
407
- return c.json({ decision: decisionSummary(decision) });
240
+ return c.json({ decision: decisionSummary(this.#boards, decision) });
408
241
  }
409
242
  });
410
243
  }
@@ -412,8 +245,31 @@ var WorkItemRoutes = class extends Route {
412
245
  routes() {
413
246
  const { audit, workItems, queueHealth, transitionService, startCoordinator, liveSessions } = this.deps;
414
247
  return [
415
- registerApiRoute("/web/factory/projects/:id/work-items", {
416
- method: "GET",
248
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.boardCatalog.path, {
249
+ method: FACTORY_ROUTE_CONTRACTS.boardCatalog.method,
250
+ requiresAuth: false,
251
+ handler: async (c) => {
252
+ const resolved = await this.#resolveProject(loose(c));
253
+ if ("response" in resolved) return resolved.response;
254
+ return c.json({ boards: [...this.#boards].map(([id, board]) => ({
255
+ id,
256
+ title: board.title,
257
+ initialPhase: board.initialPhase,
258
+ phases: Object.entries(board.phases).map(([phaseId, phase]) => ({
259
+ id: phaseId,
260
+ title: phase.title,
261
+ kind: phase.kind,
262
+ ...phase.kind === "working" ? { role: phase.role } : {},
263
+ transitions: (board.transitions[phaseId] ?? []).map(({ outcome, to }) => ({
264
+ outcome,
265
+ to
266
+ }))
267
+ }))
268
+ })) });
269
+ }
270
+ }),
271
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemList.path, {
272
+ method: FACTORY_ROUTE_CONTRACTS.workItemList.method,
417
273
  requiresAuth: false,
418
274
  handler: async (c) => {
419
275
  const resolved = await this.#resolveProject(loose(c));
@@ -425,17 +281,24 @@ var WorkItemRoutes = class extends Route {
425
281
  });
426
282
  return c.json({
427
283
  workItems: items.map(toWireWorkItem),
428
- runningSessionIds: runningSessionIds(items, liveSessions)
284
+ runningSessionIds: sessionIdsWhere(items, (sessionId) => liveSessions.isRunning(sessionId)),
285
+ parkedSessionIds: sessionIdsWhere(items, (sessionId) => liveSessions.parked(sessionId) !== void 0)
429
286
  });
430
287
  }
431
288
  }),
432
- registerApiRoute("/web/factory/projects/:id/metrics", {
433
- method: "GET",
289
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.metricsGet.path, {
290
+ method: FACTORY_ROUTE_CONTRACTS.metricsGet.method,
434
291
  requiresAuth: false,
435
292
  handler: async (c) => {
436
- const resolved = await this.#resolveProject(loose(c));
293
+ const context = loose(c);
294
+ const resolved = await this.#resolveProject(context);
437
295
  if ("response" in resolved) return resolved.response;
438
- const { windowStart, windowEnd } = parseMetricsRange(loose(c).req.query("from"), loose(c).req.query("to"), /* @__PURE__ */ new Date());
296
+ const query = FACTORY_ROUTE_CONTRACTS.metricsGet.querySchema.safeParse({
297
+ from: context.req.query("from"),
298
+ to: context.req.query("to")
299
+ });
300
+ if (!query.success) return c.json({ error: "invalid_metrics_range" }, 400);
301
+ const { windowStart, windowEnd } = parseMetricsRange(query.data.from, query.data.to, /* @__PURE__ */ new Date());
439
302
  await workItems.ensureReady();
440
303
  const items = await workItems.list({
441
304
  orgId: resolved.orgId,
@@ -447,8 +310,8 @@ var WorkItemRoutes = class extends Route {
447
310
  }) });
448
311
  }
449
312
  }),
450
- registerApiRoute("/web/factory/projects/:id/health/thresholds", {
451
- method: "GET",
313
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.healthThresholdsGet.path, {
314
+ method: FACTORY_ROUTE_CONTRACTS.healthThresholdsGet.method,
452
315
  requiresAuth: false,
453
316
  handler: async (c) => {
454
317
  const resolved = await this.#resolveProject(loose(c));
@@ -458,27 +321,33 @@ var WorkItemRoutes = class extends Route {
458
321
  return c.json({ thresholds: thresholdsOrDefault(stored) });
459
322
  }
460
323
  }),
461
- registerApiRoute("/web/factory/projects/:id/decisions", {
462
- method: "GET",
324
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.decisionList.path, {
325
+ method: FACTORY_ROUTE_CONTRACTS.decisionList.method,
463
326
  requiresAuth: false,
464
327
  handler: async (c) => {
465
328
  const context = loose(c);
466
329
  const resolved = await this.#resolveProject(context);
467
330
  if ("response" in resolved) return resolved.response;
468
- const cursorRaw = context.req.query("before");
469
- const before = parseDecisionCursor(cursorRaw);
470
- if (cursorRaw && !before) return c.json({ error: "invalid_cursor" }, 400);
331
+ const query = FACTORY_ROUTE_CONTRACTS.decisionList.querySchema.safeParse({
332
+ statuses: context.req.query("statuses"),
333
+ before: context.req.query("before"),
334
+ limit: context.req.query("limit")
335
+ });
336
+ if (!query.success) {
337
+ const field = query.error.issues[0]?.path[0];
338
+ return c.json({ error: field === "before" ? "invalid_cursor" : "invalid_decision_query" }, 400);
339
+ }
471
340
  await workItems.ensureReady();
472
341
  const page = await workItems.listDeferredDecisionPage({
473
342
  orgId: resolved.orgId,
474
343
  factoryProjectId: resolved.factoryProjectId,
475
- statuses: parseDecisionStatuses(context.req.query("statuses")),
476
- before,
477
- limit: parseDecisionLimit(context.req.query("limit"))
344
+ statuses: query.data.statuses,
345
+ before: query.data.before,
346
+ limit: query.data.limit
478
347
  });
479
348
  const last = page.decisions.at(-1);
480
349
  return c.json({
481
- decisions: page.decisions.map(decisionSummary),
350
+ decisions: page.decisions.map((decision) => decisionSummary(this.#boards, decision)),
482
351
  ...page.hasMore && last ? { nextCursor: encodeDecisionCursor(last) } : {}
483
352
  });
484
353
  }
@@ -486,10 +355,12 @@ var WorkItemRoutes = class extends Route {
486
355
  ...buildAttentionRoutes({
487
356
  workItems,
488
357
  comments: this.deps.comments,
358
+ liveSessions,
489
359
  resolveProject: (context) => this.#resolveProject(loose(context))
490
360
  }),
491
361
  ...buildSupervisorRoutes({
492
362
  workItems,
363
+ boards: this.#boards,
493
364
  resolveProject: (context) => this.#resolveProject(loose(context))
494
365
  }),
495
366
  this.#proposalRoute({
@@ -500,25 +371,29 @@ var WorkItemRoutes = class extends Route {
500
371
  verb: "dismiss",
501
372
  settle: workItems.dismissDeferredDecision.bind(workItems)
502
373
  }),
503
- registerApiRoute("/web/factory/projects/:id/decisions/:decisionId/retry", {
504
- method: "POST",
374
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.decisionRetry.path, {
375
+ method: FACTORY_ROUTE_CONTRACTS.decisionRetry.method,
505
376
  requiresAuth: false,
506
377
  handler: async (c) => {
507
378
  const context = loose(c);
508
379
  const resolved = await this.#resolveProject(context);
509
380
  if ("response" in resolved) return resolved.response;
510
- const decisionId = context.req.param("decisionId");
511
- if (!decisionId || !UUID_RE.test(decisionId)) return c.json({ error: "invalid_decision_id" }, 422);
381
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.decisionRetry.pathSchema.safeParse({
382
+ id: resolved.factoryProjectId,
383
+ decisionId: context.req.param("decisionId")
384
+ });
385
+ if (!parsedPath.success) return c.json({ error: "invalid_decision_id" }, 422);
386
+ const { decisionId } = parsedPath.data;
512
387
  await workItems.ensureReady();
513
388
  const current = await workItems.getDeferredDecision(resolved.orgId, resolved.factoryProjectId, decisionId);
514
389
  if (!current || current.status !== "failed" || !factoryDispatchFailureMetadata(current.failureCode).canRetry) return c.json({ error: "decision_not_retryable" }, 409);
515
390
  const decision = await workItems.retryDeferredDecision(resolved.orgId, resolved.factoryProjectId, decisionId, /* @__PURE__ */ new Date());
516
391
  if (!decision) return c.json({ error: "decision_not_retryable" }, 409);
517
- return c.json({ decision: decisionSummary(decision) });
392
+ return c.json({ decision: decisionSummary(this.#boards, decision) });
518
393
  }
519
394
  }),
520
- registerApiRoute("/web/factory/projects/:id/work-items", {
521
- method: "POST",
395
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemCreate.path, {
396
+ method: FACTORY_ROUTE_CONTRACTS.workItemCreate.method,
522
397
  requiresAuth: false,
523
398
  handler: async (c) => {
524
399
  const resolved = await this.#resolveProject(loose(c));
@@ -528,7 +403,7 @@ var WorkItemRoutes = class extends Route {
528
403
  const input = parseCreateWorkItem(body);
529
404
  if (!input) return c.json({ error: "invalid_work_item" }, 400);
530
405
  const boardId = input.board ?? (input.externalSource?.type === "pull-request" ? "review" : "work");
531
- const board = (this.deps.boardRegistry ?? createBoardRegistry()).get(boardId);
406
+ const board = this.#boards.get(boardId);
532
407
  if (!board) return c.json({
533
408
  error: "invalid_board",
534
409
  message: `Board '${boardId}' is not installed.`
@@ -625,15 +500,20 @@ var WorkItemRoutes = class extends Route {
625
500
  }
626
501
  }
627
502
  }),
628
- registerApiRoute("/web/factory/projects/:id/work-items/:workItemId/transition", {
629
- method: "POST",
503
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemTransition.path, {
504
+ method: FACTORY_ROUTE_CONTRACTS.workItemTransition.method,
630
505
  requiresAuth: false,
631
506
  handler: async (c) => {
632
507
  const resolved = await this.#resolveProject(loose(c));
633
508
  if ("response" in resolved) return resolved.response;
634
- const workItemId = loose(c).req.param("workItemId");
635
- if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: "Work item not found" }, 404);
636
- const parsed = parseTransitionBody(await readJson(loose(c)));
509
+ const context = loose(c);
510
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemTransition.pathSchema.safeParse({
511
+ id: resolved.factoryProjectId,
512
+ workItemId: context.req.param("workItemId")
513
+ });
514
+ if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
515
+ const { workItemId } = parsedPath.data;
516
+ const parsed = parseTransitionBody(await readJson(context));
637
517
  if (!parsed) return c.json({ error: "invalid_transition_request" }, 400);
638
518
  if (!transitionService) return c.json({ error: "factory_transition_unavailable" }, 503);
639
519
  await workItems.ensureReady();
@@ -663,7 +543,7 @@ var WorkItemRoutes = class extends Route {
663
543
  metadata: {
664
544
  transitionId: result.transitionId,
665
545
  ingressType: parsed.ingress.type,
666
- ruleSetVersion: transitionService.ruleSetVersion,
546
+ configVersion: transitionService.configVersion,
667
547
  ...result.status === "accepted" ? {
668
548
  to: result.stage,
669
549
  revision: result.revision
@@ -678,8 +558,8 @@ var WorkItemRoutes = class extends Route {
678
558
  return c.json({ result }, result.code === "stale" ? 409 : 422);
679
559
  }
680
560
  }),
681
- registerApiRoute("/web/factory/projects/:id/runs/start", {
682
- method: "POST",
561
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemStart.path, {
562
+ method: FACTORY_ROUTE_CONTRACTS.workItemStart.method,
683
563
  requiresAuth: false,
684
564
  handler: async (c) => {
685
565
  const resolved = await this.#resolveProject(loose(c));
@@ -718,15 +598,17 @@ var WorkItemRoutes = class extends Route {
718
598
  return c.json({ prepared }, 202);
719
599
  }
720
600
  }),
721
- registerApiRoute("/web/factory/work-items/:id", {
722
- method: "PATCH",
601
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemUpdate.path, {
602
+ method: FACTORY_ROUTE_CONTRACTS.workItemUpdate.method,
723
603
  requiresAuth: false,
724
604
  handler: async (c) => {
725
605
  const tenant = await this.#resolveTenant(loose(c));
726
606
  if ("response" in tenant) return tenant.response;
727
- const id = loose(c).req.param("id");
728
- if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
729
- const body = await readJson(loose(c));
607
+ const context = loose(c);
608
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemUpdate.pathSchema.safeParse({ id: context.req.param("id") });
609
+ if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
610
+ const { id } = parsedPath.data;
611
+ const body = await readJson(context);
730
612
  if (body === void 0) return c.json({ error: "Invalid JSON body" }, 400);
731
613
  const patch = parseUpdateWorkItem(body);
732
614
  if (!patch) return c.json({ error: "invalid_work_item_patch" }, 400);
@@ -737,7 +619,7 @@ var WorkItemRoutes = class extends Route {
737
619
  });
738
620
  if (!existing) return c.json({ error: "Work item not found" }, 404);
739
621
  if (patch.board !== void 0) {
740
- const board = (this.deps.boardRegistry ?? createBoardRegistry()).get(patch.board);
622
+ const board = this.#boards.get(patch.board);
741
623
  const stage = patch.stages?.length === 1 ? patch.stages[0] : void 0;
742
624
  if (existing.board !== null) return c.json({ error: "board_already_assigned" }, 409);
743
625
  if (!board) return c.json({ error: "board_not_installed" }, 400);
@@ -775,14 +657,16 @@ var WorkItemRoutes = class extends Route {
775
657
  }
776
658
  }
777
659
  }),
778
- registerApiRoute("/web/factory/work-items/:id", {
779
- method: "DELETE",
660
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemDelete.path, {
661
+ method: FACTORY_ROUTE_CONTRACTS.workItemDelete.method,
780
662
  requiresAuth: false,
781
663
  handler: async (c) => {
782
664
  const tenant = await this.#resolveTenant(loose(c));
783
665
  if ("response" in tenant) return tenant.response;
784
- const id = loose(c).req.param("id");
785
- if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
666
+ const context = loose(c);
667
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemDelete.pathSchema.safeParse({ id: context.req.param("id") });
668
+ if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
669
+ const { id } = parsedPath.data;
786
670
  await workItems.ensureReady();
787
671
  const deleted = await workItems.delete({
788
672
  orgId: tenant.orgId,