@mastra/factory 0.13.0-alpha.14 → 0.13.0-alpha.16
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/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +14 -0
- package/dist/factory.js.map +1 -1
- package/dist/routes/attention-activity.d.ts +2 -3
- package/dist/routes/attention-activity.d.ts.map +1 -1
- package/dist/routes/attention-activity.js +12 -3
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.d.ts +22 -0
- package/dist/routes/attention-parked.d.ts.map +1 -0
- package/dist/routes/attention-parked.js +136 -0
- package/dist/routes/attention-parked.js.map +1 -0
- package/dist/routes/attention-providers.js +8 -8
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/attention.d.ts +2 -0
- package/dist/routes/attention.d.ts.map +1 -1
- package/dist/routes/attention.js +19 -39
- package/dist/routes/attention.js.map +1 -1
- package/dist/routes/contracts.d.ts +38 -19
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +14 -14
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/surface.d.ts +3 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +1 -2
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts +2 -2
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +8 -6
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts +2 -2
- package/dist/rules/dispatch-errors.d.ts.map +1 -1
- package/dist/rules/dispatch-errors.js +10 -6
- package/dist/rules/dispatch-errors.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +6 -2
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +17 -12
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/session/live-sessions.d.ts +33 -3
- package/dist/session/live-sessions.d.ts.map +1 -1
- package/dist/session/live-sessions.js +67 -5
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +8 -4
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +20 -9
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/supervisor/health.d.ts +2 -2
- package/dist/supervisor/health.d.ts.map +1 -1
- package/dist/supervisor/health.js +13 -12
- package/dist/supervisor/health.js.map +1 -1
- package/package.json +6 -6
|
@@ -34,8 +34,8 @@ export interface FactoryHealthFinding {
|
|
|
34
34
|
title: string;
|
|
35
35
|
/** One sentence of grounded evidence: ids, timestamps, error text. */
|
|
36
36
|
evidence: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/** ISO instant the condition began, when it is age-based. */
|
|
38
|
+
beganAt: string | null;
|
|
39
39
|
suggestedRepair: FactoryHealthRepair | null;
|
|
40
40
|
}
|
|
41
41
|
export interface FactoryHealthReport {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/supervisor/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExE,OAAO,KAAK,EACV,6BAA6B,EAC7B,yBAAyB,EACzB,uBAAuB,EACvB,WAAW,EACX,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAE/C,MAAM,MAAM,wBAAwB,GAChC,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,cAAc,GACd,cAAc,GACd,aAAa,CAAC;AAElB,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mEAAmE;IACnE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/supervisor/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExE,OAAO,KAAK,EACV,6BAA6B,EAC7B,yBAAyB,EACzB,uBAAuB,EACvB,WAAW,EACX,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAE/C,MAAM,MAAM,wBAAwB,GAChC,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,cAAc,GACd,cAAc,GACd,aAAa,CAAC;AAElB,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mEAAmE;IACnE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,uBAAuB;IACtC,gGAAgG;IAChG,eAAe,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,cAAc,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,cAAc,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,yBAAyB,EAAE,uBAKvC,CAAC;AAoDF,UAAU,YAAY;IACpB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,SAAS,EAAE,6BAA6B,EAAE,CAAC;IAC3C,QAAQ,EAAE,uBAAuB,EAAE,CAAC;IACpC,aAAa,EAAE,yBAAyB,EAAE,CAAC;IAC3C;;;;OAIG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,cAAc,GAAG,SAAS,CAAC;CACnE;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,IAAI,EACT,UAAU,GAAE,uBAAmD,GAC9D,mBAAmB,CA0IrB;AAED,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,gBAAgB,EAC3B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,EAClD,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,uBAAuB,CAAA;CAAO,GACjE,OAAO,CAAC,mBAAmB,CAAC,CAY9B"}
|
|
@@ -59,6 +59,9 @@ function stageEnteredAt(item) {
|
|
|
59
59
|
const at = open ? Date.parse(open.enteredAt) : NaN;
|
|
60
60
|
return Number.isFinite(at) ? new Date(at) : null;
|
|
61
61
|
}
|
|
62
|
+
function beganAtMs(finding) {
|
|
63
|
+
return finding.beganAt === null ? Number.MAX_SAFE_INTEGER : Date.parse(finding.beganAt);
|
|
64
|
+
}
|
|
62
65
|
/** Pure: findings from rows. Exported so tests can feed fixtures directly. */
|
|
63
66
|
function computeFactoryHealth(inputs, now, thresholds = DEFAULT_HEALTH_THRESHOLDS) {
|
|
64
67
|
const itemsById = new Map(inputs.items.map((item) => [item.id, item]));
|
|
@@ -74,25 +77,23 @@ function computeFactoryHealth(inputs, now, thresholds = DEFAULT_HEALTH_THRESHOLD
|
|
|
74
77
|
for (const decision of inputs.decisions) {
|
|
75
78
|
const base = subject(decision.workItemId);
|
|
76
79
|
if (decision.status === "retry" || decision.status === "pending") {
|
|
77
|
-
|
|
78
|
-
if (overdue > thresholds.stuckDecisionMs) findings.push({
|
|
80
|
+
if (now.getTime() - decision.availableAt.getTime() > thresholds.stuckDecisionMs) findings.push({
|
|
79
81
|
kind: "decision-stuck",
|
|
80
82
|
id: `decision-stuck:${decision.id}`,
|
|
81
83
|
...base,
|
|
82
84
|
evidence: `Decision ${decision.id} (${describeDecision(decision)}) has been ${decision.status} since ${decision.availableAt.toISOString()} with ${decision.attempts} attempt(s) and was never leased; is the dispatcher running?`,
|
|
83
|
-
|
|
85
|
+
beganAt: decision.availableAt.toISOString(),
|
|
84
86
|
suggestedRepair: null
|
|
85
87
|
});
|
|
86
88
|
continue;
|
|
87
89
|
}
|
|
88
90
|
if (decision.status === "leased" && decision.leaseExpiresAt) {
|
|
89
|
-
|
|
90
|
-
if (expired > thresholds.expiredLeaseMs) findings.push({
|
|
91
|
+
if (now.getTime() - decision.leaseExpiresAt.getTime() > thresholds.expiredLeaseMs) findings.push({
|
|
91
92
|
kind: "decision-stuck",
|
|
92
93
|
id: `decision-stuck:${decision.id}`,
|
|
93
94
|
...base,
|
|
94
95
|
evidence: `Decision ${decision.id} (${describeDecision(decision)}) is still leased by ${decision.leaseOwner ?? "unknown"} though the lease expired at ${decision.leaseExpiresAt.toISOString()}; the worker likely died mid-dispatch.`,
|
|
95
|
-
|
|
96
|
+
beganAt: decision.leaseExpiresAt.toISOString(),
|
|
96
97
|
suggestedRepair: null
|
|
97
98
|
});
|
|
98
99
|
}
|
|
@@ -108,7 +109,7 @@ function computeFactoryHealth(inputs, now, thresholds = DEFAULT_HEALTH_THRESHOLD
|
|
|
108
109
|
id: `start-stalled:${start.id}`,
|
|
109
110
|
...base,
|
|
110
111
|
evidence: `Pending start ${start.id}${binding ? ` for the ${binding.role} seat` : ""} is ${start.status} since ${start.createdAt.toISOString()} after ${start.attempts} attempt(s)${start.lastError ? `: ${truncate(start.lastError)}` : "."}`,
|
|
111
|
-
|
|
112
|
+
beganAt: start.createdAt.toISOString(),
|
|
112
113
|
suggestedRepair: binding ? {
|
|
113
114
|
action: "revoke-binding",
|
|
114
115
|
bindingId: binding.id
|
|
@@ -126,7 +127,7 @@ function computeFactoryHealth(inputs, now, thresholds = DEFAULT_HEALTH_THRESHOLD
|
|
|
126
127
|
id: `seat-orphaned:${binding.id}`,
|
|
127
128
|
...subject(item ? item.id : null),
|
|
128
129
|
evidence: item ? `Binding ${binding.id} (${binding.role}) is still active though the card is in ${stage ?? item.stages.join("+")}.` : `Binding ${binding.id} (${binding.role}) is active for work item ${binding.workItemId}, which no longer exists.`,
|
|
129
|
-
|
|
130
|
+
beganAt: item ? stageEnteredAt(item)?.toISOString() ?? null : null,
|
|
130
131
|
suggestedRepair: {
|
|
131
132
|
action: "revoke-binding",
|
|
132
133
|
bindingId: binding.id
|
|
@@ -152,7 +153,7 @@ function computeFactoryHealth(inputs, now, thresholds = DEFAULT_HEALTH_THRESHOLD
|
|
|
152
153
|
id: `held-waiting:${item.id}`,
|
|
153
154
|
...base,
|
|
154
155
|
evidence: `Triaged as "${item.triageType}" and waiting for a maintainer's decision since ${enteredAt.toISOString()}.`,
|
|
155
|
-
|
|
156
|
+
beganAt: enteredAt.toISOString(),
|
|
156
157
|
suggestedRepair: {
|
|
157
158
|
action: "accept-work-item",
|
|
158
159
|
workItemId: item.id
|
|
@@ -165,7 +166,7 @@ function computeFactoryHealth(inputs, now, thresholds = DEFAULT_HEALTH_THRESHOLD
|
|
|
165
166
|
id: `seat-missing:${item.id}`,
|
|
166
167
|
...base,
|
|
167
168
|
evidence: `In ${stage} since ${enteredAt?.toISOString() ?? "unknown"} with no active seat and no decision in flight; nothing will move it.`,
|
|
168
|
-
|
|
169
|
+
beganAt: enteredAt?.toISOString() ?? null,
|
|
169
170
|
suggestedRepair: role ? {
|
|
170
171
|
action: "start-run",
|
|
171
172
|
workItemId: item.id,
|
|
@@ -178,14 +179,14 @@ function computeFactoryHealth(inputs, now, thresholds = DEFAULT_HEALTH_THRESHOLD
|
|
|
178
179
|
id: `label-drift:${item.id}`,
|
|
179
180
|
...base,
|
|
180
181
|
evidence: `Accepted at ${item.acceptedAt.toISOString()} but the last observed labels still include "${NEEDS_APPROVAL_LABEL}".`,
|
|
181
|
-
|
|
182
|
+
beganAt: item.acceptedAt.toISOString(),
|
|
182
183
|
suggestedRepair: {
|
|
183
184
|
action: "reconcile-labels",
|
|
184
185
|
workItemId: item.id
|
|
185
186
|
}
|
|
186
187
|
});
|
|
187
188
|
}
|
|
188
|
-
findings.sort((a, b) => (
|
|
189
|
+
findings.sort((a, b) => beganAtMs(a) - beganAtMs(b) || a.id.localeCompare(b.id));
|
|
189
190
|
const counts = Object.fromEntries(FINDING_KINDS.map((kind) => [kind, 0]));
|
|
190
191
|
for (const finding of findings) counts[finding.kind] += 1;
|
|
191
192
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.js","names":[],"sources":["../../src/supervisor/health.ts"],"sourcesContent":["/**\n * Deterministic Factory health check.\n *\n * Every finding is computed from storage rows alone — no model, no controller —\n * so the supervisor agent explains findings rather than inventing them, and the\n * same list can be rendered by the UI or polled by a timer. Each finding names\n * the row it came from and the standard repair a person (or the supervisor's\n * write tools) would apply.\n */\n\nimport { boardForWorkItem, workItemPhaseSemantics } from '../boards/index.js';\nimport type { BoardRegistry, PhaseSemantics } from '../boards/index.js';\nimport { factoryRuleStage, NEEDS_APPROVAL_LABEL } from '../rules/types.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryPendingStartRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\n\nexport type FactoryHealthFindingKind =\n | 'decision-stuck'\n | 'start-stalled'\n | 'seat-orphaned'\n | 'seat-missing'\n | 'held-waiting'\n | 'label-drift';\n\nexport type FactoryHealthRepair =\n | { action: 'revoke-binding'; bindingId: string }\n | { action: 'accept-work-item'; workItemId: string }\n | { action: 'start-run'; workItemId: string; role: string }\n | { action: 'reconcile-labels'; workItemId: string };\n\nexport interface FactoryHealthFinding {\n kind: FactoryHealthFindingKind;\n /** Stable per (kind, subject) so the UI and attention inbox can dedupe. */\n id: string;\n workItemId: string | null;\n /** Card number as shown on the board, when the row carries one. */\n workItemNumber: number | null;\n title: string;\n /** One sentence of grounded evidence: ids, timestamps, error text. */\n evidence: string;\n /** How long the condition has held, in ms, when it is age-based. */\n ageMs: number | null;\n suggestedRepair: FactoryHealthRepair | null;\n}\n\nexport interface FactoryHealthReport {\n checkedAt: string;\n findings: FactoryHealthFinding[];\n counts: Record<FactoryHealthFindingKind, number>;\n}\n\nexport interface FactoryHealthThresholds {\n /** A retry/pending decision whose `availableAt` is this far in the past never got picked up. */\n stuckDecisionMs: number;\n /** A lease this far past its expiry belongs to a worker that died mid-dispatch. */\n expiredLeaseMs: number;\n /** A pending start this old is not \"booting\", it is stalled. */\n stalledStartMs: number;\n /** Held cards older than this are worth nudging a maintainer about. */\n waitingOnPersonMs: number;\n}\n\nexport const DEFAULT_HEALTH_THRESHOLDS: FactoryHealthThresholds = {\n stuckDecisionMs: 10 * 60_000,\n expiredLeaseMs: 5 * 60_000,\n stalledStartMs: 10 * 60_000,\n waitingOnPersonMs: 24 * 60 * 60_000,\n};\n\nconst FINDING_KINDS: FactoryHealthFindingKind[] = [\n 'decision-stuck',\n 'start-stalled',\n 'seat-orphaned',\n 'seat-missing',\n 'held-waiting',\n 'label-drift',\n];\n\nconst IN_FLIGHT_DECISION_STATUSES = new Set(['pending', 'retry', 'leased', 'proposed']);\n\nfunction itemNumber(item: WorkItemRow | undefined): number | null {\n const number = item?.metadata?.number ?? item?.metadata?.githubIssueNumber ?? item?.metadata?.githubPullRequestNumber;\n return typeof number === 'number' ? number : null;\n}\n\nfunction itemLabels(item: WorkItemRow): string[] {\n const labels = item.metadata?.labels;\n return Array.isArray(labels) ? labels.filter((label): label is string => typeof label === 'string') : [];\n}\n\nfunction decisionType(decision: FactoryDeferredDecisionRecord): string {\n const type = decision.decision.type;\n return typeof type === 'string' ? type : 'decision';\n}\n\nfunction decisionRole(decision: FactoryDeferredDecisionRecord): string | null {\n const role = decision.decision.role;\n return typeof role === 'string' ? role : null;\n}\n\nfunction describeDecision(decision: FactoryDeferredDecisionRecord): string {\n const role = decisionRole(decision);\n return role ? `${decisionType(decision)} (${role})` : decisionType(decision);\n}\n\nfunction truncate(text: string, max = 200): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nfunction stageEnteredAt(item: WorkItemRow): Date | null {\n const open = [...item.stageHistory].reverse().find(entry => !entry.exitedAt);\n const at = open ? Date.parse(open.enteredAt) : Number.NaN;\n return Number.isFinite(at) ? new Date(at) : null;\n}\n\ninterface HealthInputs {\n items: WorkItemRow[];\n decisions: FactoryDeferredDecisionRecord[];\n bindings: FactoryRunBindingRecord[];\n pendingStarts: FactoryPendingStartRecord[];\n /**\n * What the item's installed board says its current phase means. Undefined when the\n * board is not installed, the phase is undeclared, or the row spans several stages;\n * the check then neither revokes nor starts anything on a guess.\n */\n phaseSemantics: (item: WorkItemRow) => PhaseSemantics | undefined;\n}\n\n/** Pure: findings from rows. Exported so tests can feed fixtures directly. */\nexport function computeFactoryHealth(\n inputs: HealthInputs,\n now: Date,\n thresholds: FactoryHealthThresholds = DEFAULT_HEALTH_THRESHOLDS,\n): FactoryHealthReport {\n const itemsById = new Map(inputs.items.map(item => [item.id, item]));\n const findings: FactoryHealthFinding[] = [];\n const subject = (workItemId: string | null) => {\n const item = workItemId ? itemsById.get(workItemId) : undefined;\n return { workItemId, workItemNumber: itemNumber(item), title: item?.title ?? '' };\n };\n\n for (const decision of inputs.decisions) {\n const base = subject(decision.workItemId);\n if (decision.status === 'retry' || decision.status === 'pending') {\n const overdue = now.getTime() - decision.availableAt.getTime();\n if (overdue > thresholds.stuckDecisionMs) {\n findings.push({\n kind: 'decision-stuck',\n id: `decision-stuck:${decision.id}`,\n ...base,\n evidence: `Decision ${decision.id} (${describeDecision(decision)}) has been ${decision.status} since ${decision.availableAt.toISOString()} with ${decision.attempts} attempt(s) and was never leased; is the dispatcher running?`,\n ageMs: overdue,\n suggestedRepair: null,\n });\n }\n continue;\n }\n if (decision.status === 'leased' && decision.leaseExpiresAt) {\n const expired = now.getTime() - decision.leaseExpiresAt.getTime();\n if (expired > thresholds.expiredLeaseMs) {\n findings.push({\n kind: 'decision-stuck',\n id: `decision-stuck:${decision.id}`,\n ...base,\n evidence: `Decision ${decision.id} (${describeDecision(decision)}) is still leased by ${decision.leaseOwner ?? 'unknown'} though the lease expired at ${decision.leaseExpiresAt.toISOString()}; the worker likely died mid-dispatch.`,\n ageMs: expired,\n suggestedRepair: null,\n });\n }\n }\n }\n\n const bindingsById = new Map(inputs.bindings.map(binding => [binding.id, binding]));\n for (const start of inputs.pendingStarts) {\n if (start.status === 'sent') continue;\n const binding = bindingsById.get(start.bindingId);\n const base = subject(binding?.workItemId ?? null);\n const age = now.getTime() - start.createdAt.getTime();\n if (start.status === 'failed' || age > thresholds.stalledStartMs) {\n findings.push({\n kind: 'start-stalled',\n id: `start-stalled:${start.id}`,\n ...base,\n evidence: `Pending start ${start.id}${binding ? ` for the ${binding.role} seat` : ''} is ${start.status} since ${start.createdAt.toISOString()} after ${start.attempts} attempt(s)${start.lastError ? `: ${truncate(start.lastError)}` : '.'}`,\n ageMs: age,\n suggestedRepair: binding ? { action: 'revoke-binding', bindingId: binding.id } : null,\n });\n }\n }\n\n const activeSeatsByItem = new Map<string, FactoryRunBindingRecord[]>();\n for (const binding of inputs.bindings) {\n if (binding.status !== 'active') continue;\n const item = itemsById.get(binding.workItemId);\n const stage = item ? factoryRuleStage(item.stages) : undefined;\n if (!item || inputs.phaseSemantics(item)?.kind === 'terminal') {\n findings.push({\n kind: 'seat-orphaned',\n id: `seat-orphaned:${binding.id}`,\n ...subject(item ? item.id : null),\n evidence: item\n ? `Binding ${binding.id} (${binding.role}) is still active though the card is in ${stage ?? item.stages.join('+')}.`\n : `Binding ${binding.id} (${binding.role}) is active for work item ${binding.workItemId}, which no longer exists.`,\n ageMs: now.getTime() - binding.createdAt.getTime(),\n suggestedRepair: { action: 'revoke-binding', bindingId: binding.id },\n });\n continue;\n }\n const seats = activeSeatsByItem.get(item.id) ?? [];\n seats.push(binding);\n activeSeatsByItem.set(item.id, seats);\n }\n\n const inFlightByItem = new Set(\n inputs.decisions.filter(d => d.workItemId && IN_FLIGHT_DECISION_STATUSES.has(d.status)).map(d => d.workItemId!),\n );\n for (const item of inputs.items) {\n const stage = factoryRuleStage(item.stages);\n const semantics = stage ? inputs.phaseSemantics(item) : undefined;\n if (!stage || semantics?.kind !== 'working') continue;\n const base = subject(item.id);\n const enteredAt = stageEnteredAt(item);\n const inStageMs = enteredAt ? now.getTime() - enteredAt.getTime() : null;\n\n // Work's triage seat holds a classified non-bug until a maintainer accepts it. Only\n // Work has that gate, so this is keyed on the board, not on the phase name.\n const held =\n boardForWorkItem(item) === 'work' &&\n stage === 'triage' &&\n item.triageType !== null &&\n item.triageType !== 'bug' &&\n !item.acceptedAt;\n if (held) {\n if (inStageMs !== null && inStageMs > thresholds.waitingOnPersonMs) {\n findings.push({\n kind: 'held-waiting',\n id: `held-waiting:${item.id}`,\n ...base,\n evidence: `Triaged as \"${item.triageType}\" and waiting for a maintainer's decision since ${enteredAt!.toISOString()}.`,\n ageMs: inStageMs,\n suggestedRepair: { action: 'accept-work-item', workItemId: item.id },\n });\n }\n } else if (!activeSeatsByItem.has(item.id) && !inFlightByItem.has(item.id)) {\n const role = semantics.role;\n findings.push({\n kind: 'seat-missing',\n id: `seat-missing:${item.id}`,\n ...base,\n evidence: `In ${stage} since ${enteredAt?.toISOString() ?? 'unknown'} with no active seat and no decision in flight; nothing will move it.`,\n ageMs: inStageMs,\n suggestedRepair: role ? { action: 'start-run', workItemId: item.id, role } : null,\n });\n }\n\n if (item.acceptedAt && itemLabels(item).includes(NEEDS_APPROVAL_LABEL)) {\n findings.push({\n kind: 'label-drift',\n id: `label-drift:${item.id}`,\n ...base,\n evidence: `Accepted at ${item.acceptedAt.toISOString()} but the last observed labels still include \"${NEEDS_APPROVAL_LABEL}\".`,\n ageMs: now.getTime() - item.acceptedAt.getTime(),\n suggestedRepair: { action: 'reconcile-labels', workItemId: item.id },\n });\n }\n }\n\n findings.sort((a, b) => (b.ageMs ?? 0) - (a.ageMs ?? 0));\n const counts = Object.fromEntries(FINDING_KINDS.map(kind => [kind, 0])) as Record<FactoryHealthFindingKind, number>;\n for (const finding of findings) counts[finding.kind] += 1;\n return { checkedAt: now.toISOString(), findings, counts };\n}\n\nexport async function runFactoryHealthCheck(\n workItems: WorkItemsStorage,\n boards: BoardRegistry,\n scope: { orgId: string; factoryProjectId: string },\n options: { now?: Date; thresholds?: FactoryHealthThresholds } = {},\n): Promise<FactoryHealthReport> {\n const [items, decisions, bindings, pendingStarts] = await Promise.all([\n workItems.list(scope),\n workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId),\n workItems.listRunBindings(scope.orgId, scope.factoryProjectId),\n workItems.listPendingStarts(scope.orgId, scope.factoryProjectId),\n ]);\n return computeFactoryHealth(\n { items, decisions, bindings, pendingStarts, phaseSemantics: item => workItemPhaseSemantics(boards, item) },\n options.now ?? new Date(),\n options.thresholds,\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"health.js","names":[],"sources":["../../src/supervisor/health.ts"],"sourcesContent":["/**\n * Deterministic Factory health check.\n *\n * Every finding is computed from storage rows alone — no model, no controller —\n * so the supervisor agent explains findings rather than inventing them, and the\n * same list can be rendered by the UI or polled by a timer. Each finding names\n * the row it came from and the standard repair a person (or the supervisor's\n * write tools) would apply.\n */\n\nimport { boardForWorkItem, workItemPhaseSemantics } from '../boards/index.js';\nimport type { BoardRegistry, PhaseSemantics } from '../boards/index.js';\nimport { factoryRuleStage, NEEDS_APPROVAL_LABEL } from '../rules/types.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryPendingStartRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\n\nexport type FactoryHealthFindingKind =\n | 'decision-stuck'\n | 'start-stalled'\n | 'seat-orphaned'\n | 'seat-missing'\n | 'held-waiting'\n | 'label-drift';\n\nexport type FactoryHealthRepair =\n | { action: 'revoke-binding'; bindingId: string }\n | { action: 'accept-work-item'; workItemId: string }\n | { action: 'start-run'; workItemId: string; role: string }\n | { action: 'reconcile-labels'; workItemId: string };\n\nexport interface FactoryHealthFinding {\n kind: FactoryHealthFindingKind;\n /** Stable per (kind, subject) so the UI and attention inbox can dedupe. */\n id: string;\n workItemId: string | null;\n /** Card number as shown on the board, when the row carries one. */\n workItemNumber: number | null;\n title: string;\n /** One sentence of grounded evidence: ids, timestamps, error text. */\n evidence: string;\n /** ISO instant the condition began, when it is age-based. */\n beganAt: string | null;\n suggestedRepair: FactoryHealthRepair | null;\n}\n\nexport interface FactoryHealthReport {\n checkedAt: string;\n findings: FactoryHealthFinding[];\n counts: Record<FactoryHealthFindingKind, number>;\n}\n\nexport interface FactoryHealthThresholds {\n /** A retry/pending decision whose `availableAt` is this far in the past never got picked up. */\n stuckDecisionMs: number;\n /** A lease this far past its expiry belongs to a worker that died mid-dispatch. */\n expiredLeaseMs: number;\n /** A pending start this old is not \"booting\", it is stalled. */\n stalledStartMs: number;\n /** Held cards older than this are worth nudging a maintainer about. */\n waitingOnPersonMs: number;\n}\n\nexport const DEFAULT_HEALTH_THRESHOLDS: FactoryHealthThresholds = {\n stuckDecisionMs: 10 * 60_000,\n expiredLeaseMs: 5 * 60_000,\n stalledStartMs: 10 * 60_000,\n waitingOnPersonMs: 24 * 60 * 60_000,\n};\n\nconst FINDING_KINDS: FactoryHealthFindingKind[] = [\n 'decision-stuck',\n 'start-stalled',\n 'seat-orphaned',\n 'seat-missing',\n 'held-waiting',\n 'label-drift',\n];\n\nconst IN_FLIGHT_DECISION_STATUSES = new Set(['pending', 'retry', 'leased', 'proposed']);\n\nfunction itemNumber(item: WorkItemRow | undefined): number | null {\n const number = item?.metadata?.number ?? item?.metadata?.githubIssueNumber ?? item?.metadata?.githubPullRequestNumber;\n return typeof number === 'number' ? number : null;\n}\n\nfunction itemLabels(item: WorkItemRow): string[] {\n const labels = item.metadata?.labels;\n return Array.isArray(labels) ? labels.filter((label): label is string => typeof label === 'string') : [];\n}\n\nfunction decisionType(decision: FactoryDeferredDecisionRecord): string {\n const type = decision.decision.type;\n return typeof type === 'string' ? type : 'decision';\n}\n\nfunction decisionRole(decision: FactoryDeferredDecisionRecord): string | null {\n const role = decision.decision.role;\n return typeof role === 'string' ? role : null;\n}\n\nfunction describeDecision(decision: FactoryDeferredDecisionRecord): string {\n const role = decisionRole(decision);\n return role ? `${decisionType(decision)} (${role})` : decisionType(decision);\n}\n\nfunction truncate(text: string, max = 200): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nfunction stageEnteredAt(item: WorkItemRow): Date | null {\n const open = [...item.stageHistory].reverse().find(entry => !entry.exitedAt);\n const at = open ? Date.parse(open.enteredAt) : Number.NaN;\n return Number.isFinite(at) ? new Date(at) : null;\n}\n\nfunction beganAtMs(finding: FactoryHealthFinding): number {\n return finding.beganAt === null ? Number.MAX_SAFE_INTEGER : Date.parse(finding.beganAt);\n}\n\ninterface HealthInputs {\n items: WorkItemRow[];\n decisions: FactoryDeferredDecisionRecord[];\n bindings: FactoryRunBindingRecord[];\n pendingStarts: FactoryPendingStartRecord[];\n /**\n * What the item's installed board says its current phase means. Undefined when the\n * board is not installed, the phase is undeclared, or the row spans several stages;\n * the check then neither revokes nor starts anything on a guess.\n */\n phaseSemantics: (item: WorkItemRow) => PhaseSemantics | undefined;\n}\n\n/** Pure: findings from rows. Exported so tests can feed fixtures directly. */\nexport function computeFactoryHealth(\n inputs: HealthInputs,\n now: Date,\n thresholds: FactoryHealthThresholds = DEFAULT_HEALTH_THRESHOLDS,\n): FactoryHealthReport {\n const itemsById = new Map(inputs.items.map(item => [item.id, item]));\n const findings: FactoryHealthFinding[] = [];\n const subject = (workItemId: string | null) => {\n const item = workItemId ? itemsById.get(workItemId) : undefined;\n return { workItemId, workItemNumber: itemNumber(item), title: item?.title ?? '' };\n };\n\n for (const decision of inputs.decisions) {\n const base = subject(decision.workItemId);\n if (decision.status === 'retry' || decision.status === 'pending') {\n const overdue = now.getTime() - decision.availableAt.getTime();\n if (overdue > thresholds.stuckDecisionMs) {\n findings.push({\n kind: 'decision-stuck',\n id: `decision-stuck:${decision.id}`,\n ...base,\n evidence: `Decision ${decision.id} (${describeDecision(decision)}) has been ${decision.status} since ${decision.availableAt.toISOString()} with ${decision.attempts} attempt(s) and was never leased; is the dispatcher running?`,\n beganAt: decision.availableAt.toISOString(),\n suggestedRepair: null,\n });\n }\n continue;\n }\n if (decision.status === 'leased' && decision.leaseExpiresAt) {\n const expired = now.getTime() - decision.leaseExpiresAt.getTime();\n if (expired > thresholds.expiredLeaseMs) {\n findings.push({\n kind: 'decision-stuck',\n id: `decision-stuck:${decision.id}`,\n ...base,\n evidence: `Decision ${decision.id} (${describeDecision(decision)}) is still leased by ${decision.leaseOwner ?? 'unknown'} though the lease expired at ${decision.leaseExpiresAt.toISOString()}; the worker likely died mid-dispatch.`,\n beganAt: decision.leaseExpiresAt.toISOString(),\n suggestedRepair: null,\n });\n }\n }\n }\n\n const bindingsById = new Map(inputs.bindings.map(binding => [binding.id, binding]));\n for (const start of inputs.pendingStarts) {\n if (start.status === 'sent') continue;\n const binding = bindingsById.get(start.bindingId);\n const base = subject(binding?.workItemId ?? null);\n const age = now.getTime() - start.createdAt.getTime();\n if (start.status === 'failed' || age > thresholds.stalledStartMs) {\n findings.push({\n kind: 'start-stalled',\n id: `start-stalled:${start.id}`,\n ...base,\n evidence: `Pending start ${start.id}${binding ? ` for the ${binding.role} seat` : ''} is ${start.status} since ${start.createdAt.toISOString()} after ${start.attempts} attempt(s)${start.lastError ? `: ${truncate(start.lastError)}` : '.'}`,\n beganAt: start.createdAt.toISOString(),\n suggestedRepair: binding ? { action: 'revoke-binding', bindingId: binding.id } : null,\n });\n }\n }\n\n const activeSeatsByItem = new Map<string, FactoryRunBindingRecord[]>();\n for (const binding of inputs.bindings) {\n if (binding.status !== 'active') continue;\n const item = itemsById.get(binding.workItemId);\n const stage = item ? factoryRuleStage(item.stages) : undefined;\n if (!item || inputs.phaseSemantics(item)?.kind === 'terminal') {\n findings.push({\n kind: 'seat-orphaned',\n id: `seat-orphaned:${binding.id}`,\n ...subject(item ? item.id : null),\n evidence: item\n ? `Binding ${binding.id} (${binding.role}) is still active though the card is in ${stage ?? item.stages.join('+')}.`\n : `Binding ${binding.id} (${binding.role}) is active for work item ${binding.workItemId}, which no longer exists.`,\n beganAt: item ? (stageEnteredAt(item)?.toISOString() ?? null) : null,\n suggestedRepair: { action: 'revoke-binding', bindingId: binding.id },\n });\n continue;\n }\n const seats = activeSeatsByItem.get(item.id) ?? [];\n seats.push(binding);\n activeSeatsByItem.set(item.id, seats);\n }\n\n const inFlightByItem = new Set(\n inputs.decisions.filter(d => d.workItemId && IN_FLIGHT_DECISION_STATUSES.has(d.status)).map(d => d.workItemId!),\n );\n for (const item of inputs.items) {\n const stage = factoryRuleStage(item.stages);\n const semantics = stage ? inputs.phaseSemantics(item) : undefined;\n if (!stage || semantics?.kind !== 'working') continue;\n const base = subject(item.id);\n const enteredAt = stageEnteredAt(item);\n const inStageMs = enteredAt ? now.getTime() - enteredAt.getTime() : null;\n\n // Work's triage seat holds a classified non-bug until a maintainer accepts it. Only\n // Work has that gate, so this is keyed on the board, not on the phase name.\n const held =\n boardForWorkItem(item) === 'work' &&\n stage === 'triage' &&\n item.triageType !== null &&\n item.triageType !== 'bug' &&\n !item.acceptedAt;\n if (held) {\n if (inStageMs !== null && inStageMs > thresholds.waitingOnPersonMs) {\n findings.push({\n kind: 'held-waiting',\n id: `held-waiting:${item.id}`,\n ...base,\n evidence: `Triaged as \"${item.triageType}\" and waiting for a maintainer's decision since ${enteredAt!.toISOString()}.`,\n beganAt: enteredAt!.toISOString(),\n suggestedRepair: { action: 'accept-work-item', workItemId: item.id },\n });\n }\n } else if (!activeSeatsByItem.has(item.id) && !inFlightByItem.has(item.id)) {\n const role = semantics.role;\n findings.push({\n kind: 'seat-missing',\n id: `seat-missing:${item.id}`,\n ...base,\n evidence: `In ${stage} since ${enteredAt?.toISOString() ?? 'unknown'} with no active seat and no decision in flight; nothing will move it.`,\n beganAt: enteredAt?.toISOString() ?? null,\n suggestedRepair: role ? { action: 'start-run', workItemId: item.id, role } : null,\n });\n }\n\n if (item.acceptedAt && itemLabels(item).includes(NEEDS_APPROVAL_LABEL)) {\n findings.push({\n kind: 'label-drift',\n id: `label-drift:${item.id}`,\n ...base,\n evidence: `Accepted at ${item.acceptedAt.toISOString()} but the last observed labels still include \"${NEEDS_APPROVAL_LABEL}\".`,\n beganAt: item.acceptedAt.toISOString(),\n suggestedRepair: { action: 'reconcile-labels', workItemId: item.id },\n });\n }\n }\n\n findings.sort((a, b) => beganAtMs(a) - beganAtMs(b) || a.id.localeCompare(b.id));\n const counts = Object.fromEntries(FINDING_KINDS.map(kind => [kind, 0])) as Record<FactoryHealthFindingKind, number>;\n for (const finding of findings) counts[finding.kind] += 1;\n return { checkedAt: now.toISOString(), findings, counts };\n}\n\nexport async function runFactoryHealthCheck(\n workItems: WorkItemsStorage,\n boards: BoardRegistry,\n scope: { orgId: string; factoryProjectId: string },\n options: { now?: Date; thresholds?: FactoryHealthThresholds } = {},\n): Promise<FactoryHealthReport> {\n const [items, decisions, bindings, pendingStarts] = await Promise.all([\n workItems.list(scope),\n workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId),\n workItems.listRunBindings(scope.orgId, scope.factoryProjectId),\n workItems.listPendingStarts(scope.orgId, scope.factoryProjectId),\n ]);\n return computeFactoryHealth(\n { items, decisions, bindings, pendingStarts, phaseSemantics: item => workItemPhaseSemantics(boards, item) },\n options.now ?? new Date(),\n options.thresholds,\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAmEA,MAAa,4BAAqD;CAChE,iBAAiB,KAAK;CACtB,gBAAgB,IAAI;CACpB,gBAAgB,KAAK;CACrB,mBAAmB,OAAU;AAC/B;AAEA,MAAM,gBAA4C;CAChD;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,8CAA8B,IAAI,IAAI;CAAC;CAAW;CAAS;CAAU;AAAU,CAAC;AAEtF,SAAS,WAAW,MAA8C;CAChE,MAAM,SAAS,MAAM,UAAU,UAAU,MAAM,UAAU,qBAAqB,MAAM,UAAU;CAC9F,OAAO,OAAO,WAAW,WAAW,SAAS;AAC/C;AAEA,SAAS,WAAW,MAA6B;CAC/C,MAAM,SAAS,KAAK,UAAU;CAC9B,OAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,QAAQ,UAA2B,OAAO,UAAU,QAAQ,IAAI,CAAC;AACzG;AAEA,SAAS,aAAa,UAAiD;CACrE,MAAM,OAAO,SAAS,SAAS;CAC/B,OAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;AAEA,SAAS,aAAa,UAAwD;CAC5E,MAAM,OAAO,SAAS,SAAS;CAC/B,OAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;AAEA,SAAS,iBAAiB,UAAiD;CACzE,MAAM,OAAO,aAAa,QAAQ;CAClC,OAAO,OAAO,GAAG,aAAa,QAAQ,EAAE,IAAI,KAAK,KAAK,aAAa,QAAQ;AAC7E;AAEA,SAAS,SAAS,MAAc,MAAM,KAAa;CACjD,OAAO,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK;AAC5D;AAEA,SAAS,eAAe,MAAgC;CACtD,MAAM,OAAO,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAK,UAAS,CAAC,MAAM,QAAQ;CAC3E,MAAM,KAAK,OAAO,KAAK,MAAM,KAAK,SAAS,IAAI;CAC/C,OAAO,OAAO,SAAS,EAAE,IAAI,IAAI,KAAK,EAAE,IAAI;AAC9C;AAEA,SAAS,UAAU,SAAuC;CACxD,OAAO,QAAQ,YAAY,OAAO,OAAO,mBAAmB,KAAK,MAAM,QAAQ,OAAO;AACxF;;AAgBA,SAAgB,qBACd,QACA,KACA,aAAsC,2BACjB;CACrB,MAAM,YAAY,IAAI,IAAI,OAAO,MAAM,KAAI,SAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;CACnE,MAAM,WAAmC,CAAC;CAC1C,MAAM,WAAW,eAA8B;EAC7C,MAAM,OAAO,aAAa,UAAU,IAAI,UAAU,IAAI,KAAA;EACtD,OAAO;GAAE;GAAY,gBAAgB,WAAW,IAAI;GAAG,OAAO,MAAM,SAAS;EAAG;CAClF;CAEA,KAAK,MAAM,YAAY,OAAO,WAAW;EACvC,MAAM,OAAO,QAAQ,SAAS,UAAU;EACxC,IAAI,SAAS,WAAW,WAAW,SAAS,WAAW,WAAW;GAEhE,IADgB,IAAI,QAAQ,IAAI,SAAS,YAAY,QAAQ,IAC/C,WAAW,iBACvB,SAAS,KAAK;IACZ,MAAM;IACN,IAAI,kBAAkB,SAAS;IAC/B,GAAG;IACH,UAAU,YAAY,SAAS,GAAG,IAAI,iBAAiB,QAAQ,EAAE,aAAa,SAAS,OAAO,SAAS,SAAS,YAAY,YAAY,EAAE,QAAQ,SAAS,SAAS;IACpK,SAAS,SAAS,YAAY,YAAY;IAC1C,iBAAiB;GACnB,CAAC;GAEH;EACF;EACA,IAAI,SAAS,WAAW,YAAY,SAAS,gBAC3B;OAAA,IAAI,QAAQ,IAAI,SAAS,eAAe,QAAQ,IAClD,WAAW,gBACvB,SAAS,KAAK;IACZ,MAAM;IACN,IAAI,kBAAkB,SAAS;IAC/B,GAAG;IACH,UAAU,YAAY,SAAS,GAAG,IAAI,iBAAiB,QAAQ,EAAE,uBAAuB,SAAS,cAAc,UAAU,+BAA+B,SAAS,eAAe,YAAY,EAAE;IAC9L,SAAS,SAAS,eAAe,YAAY;IAC7C,iBAAiB;GACnB,CAAC;EAAA;CAGP;CAEA,MAAM,eAAe,IAAI,IAAI,OAAO,SAAS,KAAI,YAAW,CAAC,QAAQ,IAAI,OAAO,CAAC,CAAC;CAClF,KAAK,MAAM,SAAS,OAAO,eAAe;EACxC,IAAI,MAAM,WAAW,QAAQ;EAC7B,MAAM,UAAU,aAAa,IAAI,MAAM,SAAS;EAChD,MAAM,OAAO,QAAQ,SAAS,cAAc,IAAI;EAChD,MAAM,MAAM,IAAI,QAAQ,IAAI,MAAM,UAAU,QAAQ;EACpD,IAAI,MAAM,WAAW,YAAY,MAAM,WAAW,gBAChD,SAAS,KAAK;GACZ,MAAM;GACN,IAAI,iBAAiB,MAAM;GAC3B,GAAG;GACH,UAAU,iBAAiB,MAAM,KAAK,UAAU,YAAY,QAAQ,KAAK,SAAS,GAAG,MAAM,MAAM,OAAO,SAAS,MAAM,UAAU,YAAY,EAAE,SAAS,MAAM,SAAS,aAAa,MAAM,YAAY,KAAK,SAAS,MAAM,SAAS,MAAM;GACzO,SAAS,MAAM,UAAU,YAAY;GACrC,iBAAiB,UAAU;IAAE,QAAQ;IAAkB,WAAW,QAAQ;GAAG,IAAI;EACnF,CAAC;CAEL;CAEA,MAAM,oCAAoB,IAAI,IAAuC;CACrE,KAAK,MAAM,WAAW,OAAO,UAAU;EACrC,IAAI,QAAQ,WAAW,UAAU;EACjC,MAAM,OAAO,UAAU,IAAI,QAAQ,UAAU;EAC7C,MAAM,QAAQ,OAAO,iBAAiB,KAAK,MAAM,IAAI,KAAA;EACrD,IAAI,CAAC,QAAQ,OAAO,eAAe,IAAI,CAAC,EAAE,SAAS,YAAY;GAC7D,SAAS,KAAK;IACZ,MAAM;IACN,IAAI,iBAAiB,QAAQ;IAC7B,GAAG,QAAQ,OAAO,KAAK,KAAK,IAAI;IAChC,UAAU,OACN,WAAW,QAAQ,GAAG,IAAI,QAAQ,KAAK,0CAA0C,SAAS,KAAK,OAAO,KAAK,GAAG,EAAE,KAChH,WAAW,QAAQ,GAAG,IAAI,QAAQ,KAAK,4BAA4B,QAAQ,WAAW;IAC1F,SAAS,OAAQ,eAAe,IAAI,CAAC,EAAE,YAAY,KAAK,OAAQ;IAChE,iBAAiB;KAAE,QAAQ;KAAkB,WAAW,QAAQ;IAAG;GACrE,CAAC;GACD;EACF;EACA,MAAM,QAAQ,kBAAkB,IAAI,KAAK,EAAE,KAAK,CAAC;EACjD,MAAM,KAAK,OAAO;EAClB,kBAAkB,IAAI,KAAK,IAAI,KAAK;CACtC;CAEA,MAAM,iBAAiB,IAAI,IACzB,OAAO,UAAU,QAAO,MAAK,EAAE,cAAc,4BAA4B,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAI,MAAK,EAAE,UAAW,CAChH;CACA,KAAK,MAAM,QAAQ,OAAO,OAAO;EAC/B,MAAM,QAAQ,iBAAiB,KAAK,MAAM;EAC1C,MAAM,YAAY,QAAQ,OAAO,eAAe,IAAI,IAAI,KAAA;EACxD,IAAI,CAAC,SAAS,WAAW,SAAS,WAAW;EAC7C,MAAM,OAAO,QAAQ,KAAK,EAAE;EAC5B,MAAM,YAAY,eAAe,IAAI;EACrC,MAAM,YAAY,YAAY,IAAI,QAAQ,IAAI,UAAU,QAAQ,IAAI;EAUpE,IALE,iBAAiB,IAAI,MAAM,UAC3B,UAAU,YACV,KAAK,eAAe,QACpB,KAAK,eAAe,SACpB,CAAC,KAAK,YAEF;OAAA,cAAc,QAAQ,YAAY,WAAW,mBAC/C,SAAS,KAAK;IACZ,MAAM;IACN,IAAI,gBAAgB,KAAK;IACzB,GAAG;IACH,UAAU,eAAe,KAAK,WAAW,kDAAkD,UAAW,YAAY,EAAE;IACpH,SAAS,UAAW,YAAY;IAChC,iBAAiB;KAAE,QAAQ;KAAoB,YAAY,KAAK;IAAG;GACrE,CAAC;EAAA,OAEE,IAAI,CAAC,kBAAkB,IAAI,KAAK,EAAE,KAAK,CAAC,eAAe,IAAI,KAAK,EAAE,GAAG;GAC1E,MAAM,OAAO,UAAU;GACvB,SAAS,KAAK;IACZ,MAAM;IACN,IAAI,gBAAgB,KAAK;IACzB,GAAG;IACH,UAAU,MAAM,MAAM,SAAS,WAAW,YAAY,KAAK,UAAU;IACrE,SAAS,WAAW,YAAY,KAAK;IACrC,iBAAiB,OAAO;KAAE,QAAQ;KAAa,YAAY,KAAK;KAAI;IAAK,IAAI;GAC/E,CAAC;EACH;EAEA,IAAI,KAAK,cAAc,WAAW,IAAI,CAAC,CAAC,SAAA,wBAA6B,GACnE,SAAS,KAAK;GACZ,MAAM;GACN,IAAI,eAAe,KAAK;GACxB,GAAG;GACH,UAAU,eAAe,KAAK,WAAW,YAAY,EAAE,+CAA+C,qBAAqB;GAC3H,SAAS,KAAK,WAAW,YAAY;GACrC,iBAAiB;IAAE,QAAQ;IAAoB,YAAY,KAAK;GAAG;EACrE,CAAC;CAEL;CAEA,SAAS,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,UAAU,CAAC,KAAK,EAAE,GAAG,cAAc,EAAE,EAAE,CAAC;CAC/E,MAAM,SAAS,OAAO,YAAY,cAAc,KAAI,SAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;CACtE,KAAK,MAAM,WAAW,UAAU,OAAO,QAAQ,SAAS;CACxD,OAAO;EAAE,WAAW,IAAI,YAAY;EAAG;EAAU;CAAO;AAC1D;AAEA,eAAsB,sBACpB,WACA,QACA,OACA,UAAgE,CAAC,GACnC;CAC9B,MAAM,CAAC,OAAO,WAAW,UAAU,iBAAiB,MAAM,QAAQ,IAAI;EACpE,UAAU,KAAK,KAAK;EACpB,UAAU,sBAAsB,MAAM,OAAO,MAAM,gBAAgB;EACnE,UAAU,gBAAgB,MAAM,OAAO,MAAM,gBAAgB;EAC7D,UAAU,kBAAkB,MAAM,OAAO,MAAM,gBAAgB;CACjE,CAAC;CACD,OAAO,qBACL;EAAE;EAAO;EAAW;EAAU;EAAe,iBAAgB,SAAQ,uBAAuB,QAAQ,IAAI;CAAE,GAC1G,QAAQ,uBAAO,IAAI,KAAK,GACxB,QAAQ,UACV;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/factory",
|
|
3
|
-
"version": "0.13.0-alpha.
|
|
3
|
+
"version": "0.13.0-alpha.16",
|
|
4
4
|
"description": "Mastra Software Factory module: the server core behind the Mastra Software Factory — storage domains, integrations, and surfaces for agent-powered software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"zod": "^4.3.6",
|
|
59
59
|
"@mastra/auth-studio": "1.3.5",
|
|
60
60
|
"@mastra/auth-workos": "1.6.5",
|
|
61
|
-
"@mastra/code-sdk": "1.7.0-alpha.11",
|
|
62
61
|
"@mastra/slack": "1.6.3",
|
|
63
|
-
"@mastra/core": "1.65.0-alpha.10"
|
|
62
|
+
"@mastra/core": "1.65.0-alpha.10",
|
|
63
|
+
"@mastra/code-sdk": "1.7.0-alpha.13"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "22.20.1",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"vitest": "4.1.10",
|
|
72
72
|
"@internal/lint": "0.0.130",
|
|
73
73
|
"@mastra/libsql": "1.22.4-alpha.0",
|
|
74
|
-
"@mastra/pg": "1.23.0-alpha.
|
|
75
|
-
"@internal/
|
|
76
|
-
"@internal/
|
|
74
|
+
"@mastra/pg": "1.23.0-alpha.5",
|
|
75
|
+
"@internal/types-builder": "0.0.105",
|
|
76
|
+
"@internal/workspace": "0.0.2"
|
|
77
77
|
},
|
|
78
78
|
"engines": {
|
|
79
79
|
"node": ">=22.19.0"
|