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