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