@mastra/factory 0.13.0-alpha.13 → 0.13.0-alpha.15

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 (70) hide show
  1. package/dist/boards/review.d.ts.map +1 -1
  2. package/dist/boards/review.js +12 -4
  3. package/dist/boards/review.js.map +1 -1
  4. package/dist/factory.d.ts.map +1 -1
  5. package/dist/factory.js +14 -0
  6. package/dist/factory.js.map +1 -1
  7. package/dist/integrations/github/sandbox.d.ts +6 -3
  8. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  9. package/dist/integrations/github/sandbox.js +21 -2
  10. package/dist/integrations/github/sandbox.js.map +1 -1
  11. package/dist/routes/attention-activity.d.ts +2 -3
  12. package/dist/routes/attention-activity.d.ts.map +1 -1
  13. package/dist/routes/attention-activity.js +12 -3
  14. package/dist/routes/attention-activity.js.map +1 -1
  15. package/dist/routes/attention-parked.d.ts +22 -0
  16. package/dist/routes/attention-parked.d.ts.map +1 -0
  17. package/dist/routes/attention-parked.js +136 -0
  18. package/dist/routes/attention-parked.js.map +1 -0
  19. package/dist/routes/attention-providers.js +8 -8
  20. package/dist/routes/attention-providers.js.map +1 -1
  21. package/dist/routes/attention.d.ts +2 -0
  22. package/dist/routes/attention.d.ts.map +1 -1
  23. package/dist/routes/attention.js +47 -118
  24. package/dist/routes/attention.js.map +1 -1
  25. package/dist/routes/contracts.d.ts +620 -0
  26. package/dist/routes/contracts.d.ts.map +1 -0
  27. package/dist/routes/contracts.js +450 -0
  28. package/dist/routes/contracts.js.map +1 -0
  29. package/dist/routes/projects.d.ts.map +1 -1
  30. package/dist/routes/projects.js +26 -76
  31. package/dist/routes/projects.js.map +1 -1
  32. package/dist/routes/supervisor.d.ts.map +1 -1
  33. package/dist/routes/supervisor.js +5 -4
  34. package/dist/routes/supervisor.js.map +1 -1
  35. package/dist/routes/surface.d.ts +3 -0
  36. package/dist/routes/surface.d.ts.map +1 -1
  37. package/dist/routes/surface.js +1 -2
  38. package/dist/routes/surface.js.map +1 -1
  39. package/dist/routes/work-items.d.ts +2 -2
  40. package/dist/routes/work-items.d.ts.map +1 -1
  41. package/dist/routes/work-items.js +94 -235
  42. package/dist/routes/work-items.js.map +1 -1
  43. package/dist/rules/dispatch-errors.d.ts +2 -2
  44. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  45. package/dist/rules/dispatch-errors.js +10 -6
  46. package/dist/rules/dispatch-errors.js.map +1 -1
  47. package/dist/rules/dispatcher.d.ts +6 -2
  48. package/dist/rules/dispatcher.d.ts.map +1 -1
  49. package/dist/rules/dispatcher.js +17 -12
  50. package/dist/rules/dispatcher.js.map +1 -1
  51. package/dist/session/live-sessions.d.ts +33 -3
  52. package/dist/session/live-sessions.d.ts.map +1 -1
  53. package/dist/session/live-sessions.js +67 -5
  54. package/dist/session/live-sessions.js.map +1 -1
  55. package/dist/storage/domains/work-items/base.d.ts +8 -4
  56. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  57. package/dist/storage/domains/work-items/base.js +20 -9
  58. package/dist/storage/domains/work-items/base.js.map +1 -1
  59. package/dist/supervisor/health.d.ts +2 -2
  60. package/dist/supervisor/health.d.ts.map +1 -1
  61. package/dist/supervisor/health.js +13 -12
  62. package/dist/supervisor/health.js.map +1 -1
  63. package/dist/work-item-branch.d.ts +2 -0
  64. package/dist/work-item-branch.d.ts.map +1 -1
  65. package/dist/work-item-branch.js +6 -1
  66. package/dist/work-item-branch.js.map +1 -1
  67. package/dist/workspace.d.ts.map +1 -1
  68. package/dist/workspace.js +3 -1
  69. package/dist/workspace.js.map +1 -1
  70. package/package.json +7 -6
@@ -1,6 +1,7 @@
1
1
  import { createBoardRegistry } from "../boards/registry.js";
2
2
  import "../boards/index.js";
3
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";
4
5
  import { Route } from "./route.js";
5
6
  import { factoryDispatchFailureMetadata } from "../rules/dispatch-errors.js";
6
7
  import { FactoryStartTransitionError } from "../rules/start-coordinator.js";
@@ -19,133 +20,29 @@ function toWireWorkItem(item) {
19
20
  metadata: publicWorkItemMetadata(item.metadata) ?? {}
20
21
  };
21
22
  }
22
- /** Session ids of the listed cards whose agent run is in flight. */
23
- function runningSessionIds(items, liveSessions) {
24
- const running = /* @__PURE__ */ new Set();
25
- for (const item of items) for (const { sessionId } of Object.values(item.sessions)) if (liveSessions.isRunning(sessionId)) running.add(sessionId);
26
- 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];
27
28
  }
28
29
  function loose(c) {
29
30
  return c;
30
31
  }
31
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
32
- const MAX_STAGES = 16;
33
- const MAX_STAGE_LENGTH = 64;
34
- const MAX_METADATA_BYTES = 16 * 1024;
35
- function isRecord(value) {
36
- return typeof value === "object" && value !== null && !Array.isArray(value);
37
- }
38
- function validStages(value) {
39
- 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;
40
- }
41
- function validMetadata(value) {
42
- if (value === null) return true;
43
- if (!isRecord(value)) return false;
44
- try {
45
- return JSON.stringify(value).length <= MAX_METADATA_BYTES;
46
- } catch {
47
- return false;
48
- }
49
- }
50
32
  function publicWorkItemMetadata(value) {
51
33
  if (value === null) return null;
52
34
  const { [FACTORY_RULE_MATERIALIZATION_KEY]: _materialization, [FACTORY_PULL_REQUEST_RECONCILIATION_KEY]: _reconciliation, ...metadata } = value;
53
35
  return metadata;
54
36
  }
55
- function parseExternalSource(value) {
56
- if (value === void 0 || value === null) return value;
57
- if (!isRecord(value)) return void 0;
58
- const { integrationId, type, externalId, url } = value;
59
- if (typeof integrationId !== "string" || integrationId.length === 0 || integrationId.length > 128) return void 0;
60
- if (typeof type !== "string" || type.length === 0 || type.length > 128) return void 0;
61
- if (typeof externalId !== "string" || externalId.length === 0 || externalId.length > 512) return void 0;
62
- if (url !== void 0 && (typeof url !== "string" || url.length > 2048)) return void 0;
63
- return {
64
- integrationId,
65
- type,
66
- externalId,
67
- ...url !== void 0 ? { url } : {}
68
- };
69
- }
70
- function parseParentWorkItemId(value) {
71
- if (value === null) return null;
72
- if (typeof value !== "string" || !UUID_RE.test(value)) return void 0;
73
- return value;
74
- }
75
- function parseSessions(value) {
76
- if (!isRecord(value)) return void 0;
77
- const out = {};
78
- for (const [role, session] of Object.entries(value)) {
79
- if (!role || role.length > 64 || !isRecord(session)) return void 0;
80
- const { sessionId, branch, threadId } = session;
81
- if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 512) return void 0;
82
- if (typeof branch !== "string" || branch.length === 0 || branch.length > 512) return void 0;
83
- if (typeof threadId !== "string" || threadId.length === 0 || threadId.length > 512) return void 0;
84
- out[role] = {
85
- sessionId,
86
- branch,
87
- threadId
88
- };
89
- }
90
- return out;
91
- }
92
37
  /** Validate an untrusted create body. Unknown keys are dropped. */
93
38
  function parseCreateWorkItem(body) {
94
- if (!isRecord(body)) return null;
95
- const { board, externalSource, title, stages, sessions, metadata } = body;
96
- if (typeof title !== "string" || title.trim().length === 0 || title.length > 500) return null;
97
- if (board !== void 0 && (typeof board !== "string" || board.length === 0 || board.length > 128)) return null;
98
- const hasParentWorkItemId = "parentWorkItemId" in body;
99
- const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
100
- if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
101
- const parsedSource = parseExternalSource(externalSource);
102
- if (externalSource !== void 0 && parsedSource === void 0) return null;
103
- if (stages !== void 0 && !validStages(stages)) return null;
104
- const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
105
- if (sessions !== void 0 && parsedSessions === void 0) return null;
106
- let parsedMetadata;
107
- if (metadata !== void 0) {
108
- if (!validMetadata(metadata)) return null;
109
- parsedMetadata = publicWorkItemMetadata(metadata);
110
- }
111
- return {
112
- title: title.trim(),
113
- ...board !== void 0 ? { board } : {},
114
- ...parsedSource !== void 0 ? { externalSource: parsedSource } : {},
115
- ...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
116
- ...stages !== void 0 ? { stages } : {},
117
- ...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
118
- ...parsedMetadata !== void 0 ? { metadata: parsedMetadata } : {}
119
- };
39
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemCreate.bodySchema.safeParse(body);
40
+ return parsed.success ? parsed.data : null;
120
41
  }
121
42
  /** Validate an untrusted patch body. Unknown keys are dropped. */
122
43
  function parseUpdateWorkItem(body) {
123
- if (!isRecord(body)) return null;
124
- const { board, title, stages, sessions, metadata, plansPreapproved } = body;
125
- const hasParentWorkItemId = "parentWorkItemId" in body;
126
- if (board === void 0 && title === void 0 && stages === void 0 && sessions === void 0 && metadata === void 0 && plansPreapproved === void 0 && !hasParentWorkItemId) return null;
127
- if (board !== void 0 && (typeof board !== "string" || board.length === 0 || board.length > 128)) return null;
128
- if (plansPreapproved !== void 0 && plansPreapproved !== true) return null;
129
- const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
130
- if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
131
- if (title !== void 0 && (typeof title !== "string" || title.trim().length === 0 || title.length > 500)) return null;
132
- if (stages !== void 0 && !validStages(stages)) return null;
133
- const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
134
- if (sessions !== void 0 && parsedSessions === void 0) return null;
135
- let parsedMetadata;
136
- if (metadata !== void 0) {
137
- if (!validMetadata(metadata)) return null;
138
- parsedMetadata = publicWorkItemMetadata(metadata);
139
- }
140
- return {
141
- ...board !== void 0 ? { board } : {},
142
- ...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
143
- ...title !== void 0 ? { title: title.trim() } : {},
144
- ...stages !== void 0 ? { stages } : {},
145
- ...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
146
- ...parsedMetadata !== void 0 ? { metadata: parsedMetadata } : {},
147
- ...plansPreapproved === true ? { plansPreapproved: true } : {}
148
- };
44
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemUpdate.bodySchema.safeParse(body);
45
+ return parsed.success ? parsed.data : null;
149
46
  }
150
47
  async function readJson(c) {
151
48
  try {
@@ -158,92 +55,21 @@ async function readJson(c) {
158
55
  function patchedFields(patch) {
159
56
  return Object.keys(patch).filter((key) => patch[key] !== void 0);
160
57
  }
161
- function boundedText(value, max) {
162
- if (typeof value !== "string") return void 0;
163
- const normalized = value.trim();
164
- return normalized.length > 0 && normalized.length <= max ? normalized : void 0;
165
- }
166
58
  function parseTransitionBody(body) {
167
- if (!isRecord(body)) return null;
168
- const board = typeof body.board === "string" && body.board.length > 0 ? body.board : void 0;
169
- const stage = typeof body.stage === "string" && body.stage.length > 0 ? body.stage : void 0;
170
- const requestId = boundedText(body.requestId, 256);
171
- const cause = boundedText(body.cause, 256);
172
- if (!board || !stage || !requestId || !UUID_RE.test(requestId) || !cause || !Number.isInteger(body.expectedRevision) || Number(body.expectedRevision) < 1) return null;
173
- return {
174
- board,
175
- stage,
176
- expectedRevision: Number(body.expectedRevision),
177
- ingress: {
178
- type: "human",
179
- identity: requestId
180
- },
181
- cause,
182
- ...body.reenter === true ? { reenter: true } : {}
183
- };
59
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemTransition.bodySchema.safeParse(body);
60
+ return parsed.success ? parsed.data : null;
184
61
  }
185
62
  function parseStartBody(body, tenant, factoryProjectId) {
186
- if (!isRecord(body) || !isRecord(body.workItem)) return null;
187
- const input = parseCreateWorkItem(body.workItem.input);
188
- const sessionId = boundedText(body.sessionId, 256);
189
- const threadTitle = boundedText(body.threadTitle, 512);
190
- const kickoffKey = boundedText(body.kickoffKey, 256);
191
- const role = boundedText(body.workItem.role, 32);
192
- const id = boundedText(body.workItem.id, 64);
193
- if (!input || !sessionId || !UUID_RE.test(sessionId) || !threadTitle || !kickoffKey || !UUID_RE.test(kickoffKey) || !id || !UUID_RE.test(id) || !role) return null;
194
- return {
63
+ const parsed = FACTORY_ROUTE_CONTRACTS.workItemStart.bodySchema.safeParse(body);
64
+ return parsed.success ? {
195
65
  ...tenant,
196
66
  factoryProjectId,
197
- sessionId,
198
- threadTitle,
199
- kickoffKey,
200
- workItem: {
201
- id,
202
- role,
203
- input
204
- }
205
- };
206
- }
207
- const DECISION_STATUSES = /* @__PURE__ */ new Set([
208
- "pending",
209
- "proposed",
210
- "dismissed",
211
- "superseded",
212
- "leased",
213
- "retry",
214
- "succeeded",
215
- "failed"
216
- ]);
217
- const DEFAULT_DECISION_PAGE_SIZE = 25;
218
- const MAX_DECISION_PAGE_SIZE = 50;
219
- function parseDecisionStatuses(raw) {
220
- if (!raw) return void 0;
221
- const statuses = [...new Set(raw.split(",").map((status) => status.trim()))].filter((status) => DECISION_STATUSES.has(status));
222
- return statuses.length > 0 ? statuses : void 0;
223
- }
224
- function parseDecisionLimit(raw) {
225
- const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_DECISION_PAGE_SIZE;
226
- if (!Number.isFinite(parsed)) return DEFAULT_DECISION_PAGE_SIZE;
227
- return Math.max(1, Math.min(MAX_DECISION_PAGE_SIZE, parsed));
67
+ ...parsed.data
68
+ } : null;
228
69
  }
229
70
  function encodeDecisionCursor(decision) {
230
71
  return Buffer.from(JSON.stringify([decision.createdAt.toISOString(), decision.id]), "utf8").toString("base64url");
231
72
  }
232
- function parseDecisionCursor(raw) {
233
- if (!raw) return void 0;
234
- try {
235
- const decoded = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
236
- if (!Array.isArray(decoded) || decoded.length !== 2 || typeof decoded[0] !== "string" || typeof decoded[1] !== "string") return;
237
- const createdAt = new Date(decoded[0]);
238
- if (Number.isNaN(createdAt.getTime()) || !UUID_RE.test(decoded[1])) return void 0;
239
- return {
240
- createdAt,
241
- id: decoded[1]
242
- };
243
- } catch {
244
- return;
245
- }
246
- }
247
73
  /** A proposed transition names the seat its lane addresses, so the card can label what approving starts. */
248
74
  function summaryRole(boards, decision) {
249
75
  if (typeof decision.role === "string") return decision.role.slice(0, 32);
@@ -304,8 +130,9 @@ var WorkItemRoutes = class extends Route {
304
130
  async #resolveProject(c) {
305
131
  const tenant = await this.#resolveTenant(c);
306
132
  if ("response" in tenant) return tenant;
307
- const projectId = c.req.param("id");
308
- 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;
309
136
  const { projects } = this.deps;
310
137
  await projects.ensureReady();
311
138
  const project = await projects.get({
@@ -374,15 +201,20 @@ var WorkItemRoutes = class extends Route {
374
201
  /** Releasing a parked run and dropping it: same request, opposite outcomes, both audited as consent. */
375
202
  #proposalRoute({ verb, settle }) {
376
203
  const { audit, workItems } = this.deps;
377
- return registerApiRoute(`/web/factory/projects/:id/decisions/:decisionId/${verb}`, {
378
- method: "POST",
204
+ const contract = verb === "approve" ? FACTORY_ROUTE_CONTRACTS.decisionApprove : FACTORY_ROUTE_CONTRACTS.decisionDismiss;
205
+ return registerApiRoute(contract.path, {
206
+ method: contract.method,
379
207
  requiresAuth: false,
380
208
  handler: async (c) => {
381
209
  const context = loose(c);
382
210
  const resolved = await this.#resolveProject(context);
383
211
  if ("response" in resolved) return resolved.response;
384
- const decisionId = context.req.param("decisionId");
385
- 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;
386
218
  await workItems.ensureReady();
387
219
  const now = /* @__PURE__ */ new Date();
388
220
  const decision = await settle(resolved.orgId, resolved.factoryProjectId, decisionId, now, resolved.userId);
@@ -413,8 +245,8 @@ var WorkItemRoutes = class extends Route {
413
245
  routes() {
414
246
  const { audit, workItems, queueHealth, transitionService, startCoordinator, liveSessions } = this.deps;
415
247
  return [
416
- registerApiRoute("/web/factory/projects/:id/work-items", {
417
- method: "GET",
248
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemList.path, {
249
+ method: FACTORY_ROUTE_CONTRACTS.workItemList.method,
418
250
  requiresAuth: false,
419
251
  handler: async (c) => {
420
252
  const resolved = await this.#resolveProject(loose(c));
@@ -426,17 +258,24 @@ var WorkItemRoutes = class extends Route {
426
258
  });
427
259
  return c.json({
428
260
  workItems: items.map(toWireWorkItem),
429
- runningSessionIds: runningSessionIds(items, liveSessions)
261
+ runningSessionIds: sessionIdsWhere(items, (sessionId) => liveSessions.isRunning(sessionId)),
262
+ parkedSessionIds: sessionIdsWhere(items, (sessionId) => liveSessions.parked(sessionId) !== void 0)
430
263
  });
431
264
  }
432
265
  }),
433
- registerApiRoute("/web/factory/projects/:id/metrics", {
434
- method: "GET",
266
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.metricsGet.path, {
267
+ method: FACTORY_ROUTE_CONTRACTS.metricsGet.method,
435
268
  requiresAuth: false,
436
269
  handler: async (c) => {
437
- const resolved = await this.#resolveProject(loose(c));
270
+ const context = loose(c);
271
+ const resolved = await this.#resolveProject(context);
438
272
  if ("response" in resolved) return resolved.response;
439
- const { windowStart, windowEnd } = parseMetricsRange(loose(c).req.query("from"), loose(c).req.query("to"), /* @__PURE__ */ new Date());
273
+ const query = FACTORY_ROUTE_CONTRACTS.metricsGet.querySchema.safeParse({
274
+ from: context.req.query("from"),
275
+ to: context.req.query("to")
276
+ });
277
+ if (!query.success) return c.json({ error: "invalid_metrics_range" }, 400);
278
+ const { windowStart, windowEnd } = parseMetricsRange(query.data.from, query.data.to, /* @__PURE__ */ new Date());
440
279
  await workItems.ensureReady();
441
280
  const items = await workItems.list({
442
281
  orgId: resolved.orgId,
@@ -448,8 +287,8 @@ var WorkItemRoutes = class extends Route {
448
287
  }) });
449
288
  }
450
289
  }),
451
- registerApiRoute("/web/factory/projects/:id/health/thresholds", {
452
- method: "GET",
290
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.healthThresholdsGet.path, {
291
+ method: FACTORY_ROUTE_CONTRACTS.healthThresholdsGet.method,
453
292
  requiresAuth: false,
454
293
  handler: async (c) => {
455
294
  const resolved = await this.#resolveProject(loose(c));
@@ -459,23 +298,29 @@ var WorkItemRoutes = class extends Route {
459
298
  return c.json({ thresholds: thresholdsOrDefault(stored) });
460
299
  }
461
300
  }),
462
- registerApiRoute("/web/factory/projects/:id/decisions", {
463
- method: "GET",
301
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.decisionList.path, {
302
+ method: FACTORY_ROUTE_CONTRACTS.decisionList.method,
464
303
  requiresAuth: false,
465
304
  handler: async (c) => {
466
305
  const context = loose(c);
467
306
  const resolved = await this.#resolveProject(context);
468
307
  if ("response" in resolved) return resolved.response;
469
- const cursorRaw = context.req.query("before");
470
- const before = parseDecisionCursor(cursorRaw);
471
- if (cursorRaw && !before) return c.json({ error: "invalid_cursor" }, 400);
308
+ const query = FACTORY_ROUTE_CONTRACTS.decisionList.querySchema.safeParse({
309
+ statuses: context.req.query("statuses"),
310
+ before: context.req.query("before"),
311
+ limit: context.req.query("limit")
312
+ });
313
+ if (!query.success) {
314
+ const field = query.error.issues[0]?.path[0];
315
+ return c.json({ error: field === "before" ? "invalid_cursor" : "invalid_decision_query" }, 400);
316
+ }
472
317
  await workItems.ensureReady();
473
318
  const page = await workItems.listDeferredDecisionPage({
474
319
  orgId: resolved.orgId,
475
320
  factoryProjectId: resolved.factoryProjectId,
476
- statuses: parseDecisionStatuses(context.req.query("statuses")),
477
- before,
478
- limit: parseDecisionLimit(context.req.query("limit"))
321
+ statuses: query.data.statuses,
322
+ before: query.data.before,
323
+ limit: query.data.limit
479
324
  });
480
325
  const last = page.decisions.at(-1);
481
326
  return c.json({
@@ -487,6 +332,7 @@ var WorkItemRoutes = class extends Route {
487
332
  ...buildAttentionRoutes({
488
333
  workItems,
489
334
  comments: this.deps.comments,
335
+ liveSessions,
490
336
  resolveProject: (context) => this.#resolveProject(loose(context))
491
337
  }),
492
338
  ...buildSupervisorRoutes({
@@ -502,15 +348,19 @@ var WorkItemRoutes = class extends Route {
502
348
  verb: "dismiss",
503
349
  settle: workItems.dismissDeferredDecision.bind(workItems)
504
350
  }),
505
- registerApiRoute("/web/factory/projects/:id/decisions/:decisionId/retry", {
506
- method: "POST",
351
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.decisionRetry.path, {
352
+ method: FACTORY_ROUTE_CONTRACTS.decisionRetry.method,
507
353
  requiresAuth: false,
508
354
  handler: async (c) => {
509
355
  const context = loose(c);
510
356
  const resolved = await this.#resolveProject(context);
511
357
  if ("response" in resolved) return resolved.response;
512
- const decisionId = context.req.param("decisionId");
513
- if (!decisionId || !UUID_RE.test(decisionId)) return c.json({ error: "invalid_decision_id" }, 422);
358
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.decisionRetry.pathSchema.safeParse({
359
+ id: resolved.factoryProjectId,
360
+ decisionId: context.req.param("decisionId")
361
+ });
362
+ if (!parsedPath.success) return c.json({ error: "invalid_decision_id" }, 422);
363
+ const { decisionId } = parsedPath.data;
514
364
  await workItems.ensureReady();
515
365
  const current = await workItems.getDeferredDecision(resolved.orgId, resolved.factoryProjectId, decisionId);
516
366
  if (!current || current.status !== "failed" || !factoryDispatchFailureMetadata(current.failureCode).canRetry) return c.json({ error: "decision_not_retryable" }, 409);
@@ -519,8 +369,8 @@ var WorkItemRoutes = class extends Route {
519
369
  return c.json({ decision: decisionSummary(this.#boards, decision) });
520
370
  }
521
371
  }),
522
- registerApiRoute("/web/factory/projects/:id/work-items", {
523
- method: "POST",
372
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemCreate.path, {
373
+ method: FACTORY_ROUTE_CONTRACTS.workItemCreate.method,
524
374
  requiresAuth: false,
525
375
  handler: async (c) => {
526
376
  const resolved = await this.#resolveProject(loose(c));
@@ -627,15 +477,20 @@ var WorkItemRoutes = class extends Route {
627
477
  }
628
478
  }
629
479
  }),
630
- registerApiRoute("/web/factory/projects/:id/work-items/:workItemId/transition", {
631
- method: "POST",
480
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemTransition.path, {
481
+ method: FACTORY_ROUTE_CONTRACTS.workItemTransition.method,
632
482
  requiresAuth: false,
633
483
  handler: async (c) => {
634
484
  const resolved = await this.#resolveProject(loose(c));
635
485
  if ("response" in resolved) return resolved.response;
636
- const workItemId = loose(c).req.param("workItemId");
637
- if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: "Work item not found" }, 404);
638
- const parsed = parseTransitionBody(await readJson(loose(c)));
486
+ const context = loose(c);
487
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemTransition.pathSchema.safeParse({
488
+ id: resolved.factoryProjectId,
489
+ workItemId: context.req.param("workItemId")
490
+ });
491
+ if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
492
+ const { workItemId } = parsedPath.data;
493
+ const parsed = parseTransitionBody(await readJson(context));
639
494
  if (!parsed) return c.json({ error: "invalid_transition_request" }, 400);
640
495
  if (!transitionService) return c.json({ error: "factory_transition_unavailable" }, 503);
641
496
  await workItems.ensureReady();
@@ -680,8 +535,8 @@ var WorkItemRoutes = class extends Route {
680
535
  return c.json({ result }, result.code === "stale" ? 409 : 422);
681
536
  }
682
537
  }),
683
- registerApiRoute("/web/factory/projects/:id/runs/start", {
684
- method: "POST",
538
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemStart.path, {
539
+ method: FACTORY_ROUTE_CONTRACTS.workItemStart.method,
685
540
  requiresAuth: false,
686
541
  handler: async (c) => {
687
542
  const resolved = await this.#resolveProject(loose(c));
@@ -720,15 +575,17 @@ var WorkItemRoutes = class extends Route {
720
575
  return c.json({ prepared }, 202);
721
576
  }
722
577
  }),
723
- registerApiRoute("/web/factory/work-items/:id", {
724
- method: "PATCH",
578
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemUpdate.path, {
579
+ method: FACTORY_ROUTE_CONTRACTS.workItemUpdate.method,
725
580
  requiresAuth: false,
726
581
  handler: async (c) => {
727
582
  const tenant = await this.#resolveTenant(loose(c));
728
583
  if ("response" in tenant) return tenant.response;
729
- const id = loose(c).req.param("id");
730
- if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
731
- const body = await readJson(loose(c));
584
+ const context = loose(c);
585
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemUpdate.pathSchema.safeParse({ id: context.req.param("id") });
586
+ if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
587
+ const { id } = parsedPath.data;
588
+ const body = await readJson(context);
732
589
  if (body === void 0) return c.json({ error: "Invalid JSON body" }, 400);
733
590
  const patch = parseUpdateWorkItem(body);
734
591
  if (!patch) return c.json({ error: "invalid_work_item_patch" }, 400);
@@ -777,14 +634,16 @@ var WorkItemRoutes = class extends Route {
777
634
  }
778
635
  }
779
636
  }),
780
- registerApiRoute("/web/factory/work-items/:id", {
781
- method: "DELETE",
637
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemDelete.path, {
638
+ method: FACTORY_ROUTE_CONTRACTS.workItemDelete.method,
782
639
  requiresAuth: false,
783
640
  handler: async (c) => {
784
641
  const tenant = await this.#resolveTenant(loose(c));
785
642
  if ("response" in tenant) return tenant.response;
786
- const id = loose(c).req.param("id");
787
- if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
643
+ const context = loose(c);
644
+ const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemDelete.pathSchema.safeParse({ id: context.req.param("id") });
645
+ if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
646
+ const { id } = parsedPath.data;
788
647
  await workItems.ensureReady();
789
648
  const deleted = await workItems.delete({
790
649
  orgId: tenant.orgId,