@mastra/factory 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150
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 +695 -0
- package/LICENSE.md +30 -0
- package/README.md +27 -0
- package/dist/auth.d.ts +181 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +505 -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/intake.d.ts +140 -0
- package/dist/capabilities/intake.d.ts.map +1 -0
- package/dist/capabilities/intake.js +1 -0
- package/dist/capabilities/version-control.d.ts +245 -0
- package/dist/capabilities/version-control.d.ts.map +1 -0
- package/dist/capabilities/version-control.js +1 -0
- package/dist/factory.d.ts +156 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +483 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/integrations/base.d.ts +210 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +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 +42 -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 +37 -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 +1077 -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/pat.d.ts +38 -0
- package/dist/integrations/github/pat.d.ts.map +1 -0
- package/dist/integrations/github/pat.js +59 -0
- package/dist/integrations/github/pat.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 +75 -0
- package/dist/integrations/github/provenance.js.map +1 -0
- package/dist/integrations/github/routes.d.ts +69 -0
- package/dist/integrations/github/routes.d.ts.map +1 -0
- package/dist/integrations/github/routes.js +1324 -0
- package/dist/integrations/github/routes.js.map +1 -0
- package/dist/integrations/github/rules.d.ts +78 -0
- package/dist/integrations/github/rules.d.ts.map +1 -0
- package/dist/integrations/github/rules.js +362 -0
- package/dist/integrations/github/rules.js.map +1 -0
- package/dist/integrations/github/sandbox-release.d.ts +38 -0
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
- package/dist/integrations/github/sandbox-release.js +74 -0
- package/dist/integrations/github/sandbox-release.js.map +1 -0
- package/dist/integrations/github/sandbox.d.ts +281 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -0
- package/dist/integrations/github/sandbox.js +547 -0
- package/dist/integrations/github/sandbox.js.map +1 -0
- package/dist/integrations/github/session-subscriptions.d.ts +26 -0
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/session-subscriptions.js +192 -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 +62 -0
- package/dist/integrations/github/subscriptions.js.map +1 -0
- package/dist/integrations/github/token-refresh.d.ts +12 -0
- package/dist/integrations/github/token-refresh.d.ts.map +1 -0
- package/dist/integrations/github/token-refresh.js +24 -0
- package/dist/integrations/github/token-refresh.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 +397 -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 +85 -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 +672 -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 +239 -0
- package/dist/integrations/linear/routes.js.map +1 -0
- package/dist/integrations/linear/rules.d.ts +42 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -0
- package/dist/integrations/linear/rules.js +133 -0
- package/dist/integrations/linear/rules.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/platform/api-client.d.ts +26 -0
- package/dist/integrations/platform/api-client.d.ts.map +1 -0
- package/dist/integrations/platform/api-client.js +156 -0
- package/dist/integrations/platform/api-client.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.d.ts +49 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/github/event-worker.js +348 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -0
- package/dist/integrations/platform/github/integration.d.ts +50 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -0
- package/dist/integrations/platform/github/integration.js +961 -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 +483 -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 +91 -0
- package/dist/integrations/workos/integration.js.map +1 -0
- package/dist/routes/config.d.ts +235 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +972 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/custom-provider-source.d.ts +52 -0
- package/dist/routes/custom-provider-source.d.ts.map +1 -0
- package/dist/routes/custom-provider-source.js +124 -0
- package/dist/routes/custom-provider-source.js.map +1 -0
- package/dist/routes/fs.d.ts +151 -0
- package/dist/routes/fs.d.ts.map +1 -0
- package/dist/routes/fs.js +708 -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 +463 -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 +461 -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 +86 -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 +15 -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 +168 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/surface.d.ts +91 -0
- package/dist/routes/surface.d.ts.map +1 -0
- package/dist/routes/surface.js +260 -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 +156 -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 +695 -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 +28 -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 +260 -0
- package/dist/rules/defaults.js.map +1 -0
- package/dist/rules/dispatcher.d.ts +42 -0
- package/dist/rules/dispatcher.d.ts.map +1 -0
- package/dist/rules/dispatcher.js +518 -0
- package/dist/rules/dispatcher.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 +5 -0
- package/dist/rules/processor.d.ts +54 -0
- package/dist/rules/processor.d.ts.map +1 -0
- package/dist/rules/processor.js +378 -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 +30 -0
- package/dist/rules/resolve.js.map +1 -0
- package/dist/rules/start-coordinator.d.ts +60 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -0
- package/dist/rules/start-coordinator.js +194 -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 +58 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/transition-service.d.ts +49 -0
- package/dist/rules/transition-service.d.ts.map +1 -0
- package/dist/rules/transition-service.js +232 -0
- package/dist/rules/transition-service.js.map +1 -0
- package/dist/rules/types.d.ts +234 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +38 -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 +308 -0
- package/dist/rules/validation.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +222 -0
- package/dist/sandbox/fleet.d.ts.map +1 -0
- package/dist/sandbox/fleet.js +322 -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 +16 -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 +16 -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 +40 -0
- package/dist/skills/service.js.map +1 -0
- package/dist/spa-static.d.ts +33 -0
- package/dist/spa-static.d.ts.map +1 -0
- package/dist/spa-static.js +104 -0
- package/dist/spa-static.js.map +1 -0
- package/dist/state-signing.d.ts +59 -0
- package/dist/state-signing.d.ts.map +1 -0
- package/dist/state-signing.js +83 -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 +66 -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 +184 -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 +167 -0
- package/dist/storage/domains/audit/domain.js.map +1 -0
- package/dist/storage/domains/channel-identity/base.d.ts +96 -0
- package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
- package/dist/storage/domains/channel-identity/base.js +149 -0
- package/dist/storage/domains/channel-identity/base.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 +273 -0
- package/dist/storage/domains/credentials/base.js.map +1 -0
- package/dist/storage/domains/custom-providers/base.d.ts +57 -0
- package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
- package/dist/storage/domains/custom-providers/base.js +145 -0
- package/dist/storage/domains/custom-providers/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 +80 -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 +317 -0
- package/dist/storage/domains/integrations/base.js.map +1 -0
- package/dist/storage/domains/memory-settings/base.d.ts +62 -0
- package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
- package/dist/storage/domains/memory-settings/base.js +126 -0
- package/dist/storage/domains/memory-settings/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 +96 -0
- package/dist/storage/domains/model-packs/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +58 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -0
- package/dist/storage/domains/projects/base.js +111 -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 +122 -0
- package/dist/storage/domains/queue-health/base.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +358 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -0
- package/dist/storage/domains/source-control/base.js +908 -0
- package/dist/storage/domains/source-control/base.js.map +1 -0
- package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
- package/dist/storage/domains/source-control/inmemory.js +377 -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 +1309 -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 +221 -0
- package/dist/storage/domains/work-items/metrics.js.map +1 -0
- 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 +24 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +286 -0
- package/dist/workspace.js.map +1 -0
- package/factory-skills/configure-factory-rules/SKILL.md +67 -0
- package/factory-skills/factory-plan/SKILL.md +61 -0
- package/factory-skills/factory-review/SKILL.md +155 -0
- package/factory-skills/factory-triage/SKILL.md +77 -0
- package/package.json +81 -0
|
@@ -0,0 +1,695 @@
|
|
|
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";
|
|
7
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
8
|
+
//#region src/routes/work-items.ts
|
|
9
|
+
function loose(c) {
|
|
10
|
+
return c;
|
|
11
|
+
}
|
|
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;
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function validStages(value) {
|
|
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;
|
|
21
|
+
}
|
|
22
|
+
function validMetadata(value) {
|
|
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
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function parseExternalSource(value) {
|
|
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
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function parseParentWorkItemId(value) {
|
|
47
|
+
if (value === null) return null;
|
|
48
|
+
if (typeof value !== "string" || !UUID_RE.test(value)) return void 0;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function parseSessions(value) {
|
|
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. */
|
|
69
|
+
function parseCreateWorkItem(body) {
|
|
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. */
|
|
92
|
+
function parseUpdateWorkItem(body) {
|
|
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
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function readJson(c) {
|
|
113
|
+
try {
|
|
114
|
+
return await c.req.json();
|
|
115
|
+
} catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Fields a PATCH touched, for the bounded `updated` event summary. */
|
|
120
|
+
function patchedFields(patch) {
|
|
121
|
+
return Object.keys(patch).filter((key) => patch[key] !== void 0);
|
|
122
|
+
}
|
|
123
|
+
function boundedText(value, max) {
|
|
124
|
+
if (typeof value !== "string") return void 0;
|
|
125
|
+
const normalized = value.trim();
|
|
126
|
+
return normalized.length > 0 && normalized.length <= max ? normalized : void 0;
|
|
127
|
+
}
|
|
128
|
+
function parseTransitionBody(body) {
|
|
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
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function parseInvocation(value) {
|
|
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;
|
|
166
|
+
}
|
|
167
|
+
function parseStartBody(body, tenant, factoryProjectId) {
|
|
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;
|
|
205
|
+
function parseDecisionStatuses(raw) {
|
|
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;
|
|
209
|
+
}
|
|
210
|
+
function parseDecisionLimit(raw) {
|
|
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));
|
|
214
|
+
}
|
|
215
|
+
function encodeDecisionCursor(decision) {
|
|
216
|
+
return Buffer.from(JSON.stringify([decision.createdAt.toISOString(), decision.id]), "utf8").toString("base64url");
|
|
217
|
+
}
|
|
218
|
+
function parseDecisionCursor(raw) {
|
|
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
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function decisionSummary(decision) {
|
|
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
|
+
};
|
|
247
|
+
}
|
|
248
|
+
var WorkItemRoutes = class extends Route {
|
|
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
|
+
}
|
|
691
|
+
};
|
|
692
|
+
//#endregion
|
|
693
|
+
export { WorkItemRoutes, parseCreateWorkItem, parseUpdateWorkItem };
|
|
694
|
+
|
|
695
|
+
//# sourceMappingURL=work-items.js.map
|