@mastra/factory 0.5.1-alpha.0 → 0.6.0-alpha.10
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 +211 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +19 -1
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/intake.d.ts +1 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/capabilities/version-control.d.ts +3 -0
- package/dist/capabilities/version-control.d.ts.map +1 -1
- package/dist/factory.d.ts +10 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +17 -4
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +6 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.d.ts +14 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +61 -3
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-reconciler.d.ts +24 -0
- package/dist/integrations/github/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/github/issue-reconciler.js +104 -0
- package/dist/integrations/github/issue-reconciler.js.map +1 -0
- package/dist/integrations/github/reconcile-worker.d.ts +8 -0
- package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +38 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +65 -52
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +37 -2
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +150 -14
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox-release.d.ts +16 -1
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
- package/dist/integrations/github/sandbox-release.js +42 -1
- package/dist/integrations/github/sandbox-release.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +74 -8
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +16 -12
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.d.ts +19 -0
- package/dist/integrations/issue-reconcile-worker.d.ts.map +1 -0
- package/dist/integrations/issue-reconcile-worker.js +98 -0
- package/dist/integrations/issue-reconcile-worker.js.map +1 -0
- package/dist/integrations/issue-reconciler.d.ts +47 -0
- package/dist/integrations/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/issue-reconciler.js +104 -0
- package/dist/integrations/issue-reconciler.js.map +1 -0
- package/dist/integrations/linear/integration.d.ts +4 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +19 -2
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/issue-reconciler.d.ts +6 -0
- package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/linear/issue-reconciler.js +64 -0
- package/dist/integrations/linear/issue-reconciler.js.map +1 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +1 -0
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts +2 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -1
- package/dist/integrations/linear/rules.js +5 -2
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.d.ts +7 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +24 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +13 -3
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +42 -1
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.d.ts +105 -0
- package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/linear/event-worker.js +380 -0
- package/dist/integrations/platform/linear/event-worker.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +9 -0
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +37 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +3 -2
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +37 -110
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +49 -44
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/routes/config.d.ts +25 -0
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +63 -1
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/fs.d.ts +16 -2
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +47 -5
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +13 -5
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/surface.d.ts +13 -1
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +20 -9
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/binding-context.d.ts +28 -1
- package/dist/rules/binding-context.d.ts.map +1 -1
- package/dist/rules/binding-context.js +68 -3
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.d.ts.map +1 -1
- package/dist/rules/defaults.js +71 -5
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +8 -27
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.d.ts +2 -0
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +15 -6
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/types.d.ts +17 -2
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +2 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/session/factory-session.d.ts +116 -0
- package/dist/session/factory-session.d.ts.map +1 -0
- package/dist/session/factory-session.js +162 -0
- package/dist/session/factory-session.js.map +1 -0
- package/dist/session/filesystem-capture.d.ts +26 -0
- package/dist/session/filesystem-capture.d.ts.map +1 -0
- package/dist/session/filesystem-capture.js +75 -0
- package/dist/session/filesystem-capture.js.map +1 -0
- package/dist/storage/domains/audit/domain.d.ts +9 -2
- package/dist/storage/domains/audit/domain.d.ts.map +1 -1
- package/dist/storage/domains/audit/domain.js +106 -4
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/filesystem/base.d.ts +26 -0
- package/dist/storage/domains/filesystem/base.d.ts.map +1 -0
- package/dist/storage/domains/filesystem/base.js +74 -0
- package/dist/storage/domains/filesystem/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +1 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +3 -0
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +2 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +6 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +3 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +11 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +28 -0
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +99 -45
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-triage/SKILL.md +45 -12
- package/package.json +6 -6
- package/dist/integrations/github/factory-session.d.ts +0 -12
- package/dist/integrations/github/factory-session.d.ts.map +0 -1
- package/dist/integrations/github/factory-session.js +0 -37
- package/dist/integrations/github/factory-session.js.map +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { NoopLeaseProvider, isLeaseProvider } from "@mastra/core/events";
|
|
3
|
+
import { MastraWorker } from "@mastra/core/worker";
|
|
4
|
+
//#region src/integrations/issue-reconcile-worker.ts
|
|
5
|
+
const DEFAULT_ISSUE_RECONCILE_INTERVAL_MS = 5 * 6e4;
|
|
6
|
+
const MIN_LEASE_TTL_MS = 3e4;
|
|
7
|
+
var IssueReconcileWorker = class extends MastraWorker {
|
|
8
|
+
name;
|
|
9
|
+
#integrationId;
|
|
10
|
+
#reconcile;
|
|
11
|
+
#intervalMs;
|
|
12
|
+
#leaseTtlMs;
|
|
13
|
+
#leaseKey;
|
|
14
|
+
#leaseOwner = randomUUID();
|
|
15
|
+
#running = false;
|
|
16
|
+
#timer;
|
|
17
|
+
#inFlight;
|
|
18
|
+
#leaseProvider = NoopLeaseProvider;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super();
|
|
21
|
+
this.#integrationId = config.integrationId;
|
|
22
|
+
this.name = `${config.integrationId}-issue-reconcile`;
|
|
23
|
+
this.#leaseKey = `${config.integrationId}:issue-reconcile`;
|
|
24
|
+
this.#reconcile = config.reconcile;
|
|
25
|
+
this.#intervalMs = config.intervalMs ?? 3e5;
|
|
26
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) throw new Error(`${config.integrationId} issue reconcile interval must be a positive number.`);
|
|
27
|
+
this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);
|
|
28
|
+
}
|
|
29
|
+
async init(deps) {
|
|
30
|
+
await super.init(deps);
|
|
31
|
+
this.#leaseProvider = getLeaseProvider(deps.pubsub);
|
|
32
|
+
}
|
|
33
|
+
async start() {
|
|
34
|
+
if (this.#running) return;
|
|
35
|
+
if (!this.deps) throw new Error("IssueReconcileWorker: call init() before start()");
|
|
36
|
+
this.#running = true;
|
|
37
|
+
this.deps.logger.info(`${this.#integrationId} issue reconcile worker started`, { intervalMs: this.#intervalMs });
|
|
38
|
+
this.#schedule(0);
|
|
39
|
+
}
|
|
40
|
+
async stop() {
|
|
41
|
+
if (!this.#running) return;
|
|
42
|
+
this.#running = false;
|
|
43
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
44
|
+
this.#timer = void 0;
|
|
45
|
+
await this.#inFlight;
|
|
46
|
+
}
|
|
47
|
+
get isRunning() {
|
|
48
|
+
return this.#running;
|
|
49
|
+
}
|
|
50
|
+
#schedule(delayMs) {
|
|
51
|
+
if (!this.#running) return;
|
|
52
|
+
this.#timer = setTimeout(() => {
|
|
53
|
+
this.#timer = void 0;
|
|
54
|
+
const run = this.#tick().finally(() => {
|
|
55
|
+
this.#inFlight = void 0;
|
|
56
|
+
this.#schedule(this.#intervalMs);
|
|
57
|
+
});
|
|
58
|
+
this.#inFlight = run;
|
|
59
|
+
}, delayMs);
|
|
60
|
+
this.#timer.unref?.();
|
|
61
|
+
}
|
|
62
|
+
async #tick() {
|
|
63
|
+
if (!(await this.#leaseProvider.acquireLease(this.#leaseKey, this.#leaseOwner, this.#leaseTtlMs).catch(() => ({ acquired: false }))).acquired) return;
|
|
64
|
+
const renewalTimer = setInterval(() => {
|
|
65
|
+
this.#leaseProvider.renewLease(this.#leaseKey, this.#leaseOwner, this.#leaseTtlMs).catch((error) => {
|
|
66
|
+
this.deps?.logger.warn(`${this.#integrationId} issue reconcile lease renewal failed`, { error: error instanceof Error ? error.message : String(error) });
|
|
67
|
+
});
|
|
68
|
+
}, Math.max(1e3, Math.floor(this.#leaseTtlMs / 3)));
|
|
69
|
+
renewalTimer.unref?.();
|
|
70
|
+
try {
|
|
71
|
+
const startedAt = Date.now();
|
|
72
|
+
const { errors, ...counts } = await this.#reconcile();
|
|
73
|
+
const context = {
|
|
74
|
+
...counts,
|
|
75
|
+
durationMs: Date.now() - startedAt
|
|
76
|
+
};
|
|
77
|
+
if (counts.failed > 0) this.deps?.logger.warn(`${this.#integrationId} issue reconcile sweep completed with failures`, {
|
|
78
|
+
...context,
|
|
79
|
+
errors
|
|
80
|
+
});
|
|
81
|
+
else this.deps?.logger.debug(`${this.#integrationId} issue reconcile sweep completed`, context);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.deps?.logger.warn(`${this.#integrationId} issue reconcile sweep failed`, { error: error instanceof Error ? error.message : String(error) });
|
|
84
|
+
} finally {
|
|
85
|
+
clearInterval(renewalTimer);
|
|
86
|
+
await this.#leaseProvider.releaseLease(this.#leaseKey, this.#leaseOwner).catch(() => void 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function getLeaseProvider(pubsub) {
|
|
91
|
+
const getProvider = pubsub.getLeaseProvider;
|
|
92
|
+
if (typeof getProvider === "function") return getProvider.call(pubsub) ?? NoopLeaseProvider;
|
|
93
|
+
return isLeaseProvider(pubsub) ? pubsub : pubsub.lease ?? NoopLeaseProvider;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { DEFAULT_ISSUE_RECONCILE_INTERVAL_MS, IssueReconcileWorker };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=issue-reconcile-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-reconcile-worker.js","names":["#integrationId","#reconcile","#intervalMs","#leaseTtlMs","#leaseKey","#leaseOwner","#leaseProvider","#running","#schedule","#timer","#inFlight","#tick"],"sources":["../../src/integrations/issue-reconcile-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type { IssueReconciler } from './issue-reconciler.js';\n\nexport const DEFAULT_ISSUE_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst MIN_LEASE_TTL_MS = 30_000;\n\nexport interface IssueReconcileWorkerConfig {\n integrationId: string;\n reconcile: IssueReconciler;\n intervalMs?: number;\n}\n\nexport class IssueReconcileWorker extends MastraWorker {\n readonly name: string;\n\n readonly #integrationId: string;\n readonly #reconcile: IssueReconciler;\n readonly #intervalMs: number;\n readonly #leaseTtlMs: number;\n readonly #leaseKey: string;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n\n constructor(config: IssueReconcileWorkerConfig) {\n super();\n this.#integrationId = config.integrationId;\n this.name = `${config.integrationId}-issue-reconcile`;\n this.#leaseKey = `${config.integrationId}:issue-reconcile`;\n this.#reconcile = config.reconcile;\n this.#intervalMs = config.intervalMs ?? DEFAULT_ISSUE_RECONCILE_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error(`${config.integrationId} issue reconcile interval must be a positive number.`);\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('IssueReconcileWorker: call init() before start()');\n this.#running = true;\n this.deps.logger.info(`${this.#integrationId} issue reconcile worker started`, { intervalMs: this.#intervalMs });\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n await this.#inFlight;\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick().finally(() => {\n this.#inFlight = undefined;\n this.#schedule(this.#intervalMs);\n });\n this.#inFlight = run;\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n const lease = await this.#leaseProvider\n .acquireLease(this.#leaseKey, this.#leaseOwner, this.#leaseTtlMs)\n .catch(() => ({ acquired: false }));\n if (!lease.acquired) return;\n\n // A sweep that traverses many projects/orgs can outrun the lease TTL;\n // renew periodically so a replica can't grab the expired lease and start\n // an overlapping sweep partway through this one.\n const renewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(this.#leaseKey, this.#leaseOwner, this.#leaseTtlMs)\n .catch(error => {\n this.deps?.logger.warn(`${this.#integrationId} issue reconcile lease renewal failed`, {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.max(1_000, Math.floor(this.#leaseTtlMs / 3)),\n );\n renewalTimer.unref?.();\n\n try {\n const startedAt = Date.now();\n const { errors, ...counts } = await this.#reconcile();\n const context = { ...counts, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn(`${this.#integrationId} issue reconcile sweep completed with failures`, {\n ...context,\n errors,\n });\n } else {\n this.deps?.logger.debug(`${this.#integrationId} issue reconcile sweep completed`, context);\n }\n } catch (error) {\n this.deps?.logger.warn(`${this.#integrationId} issue reconcile sweep failed`, {\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n clearInterval(renewalTimer);\n await this.#leaseProvider.releaseLease(this.#leaseKey, this.#leaseOwner).catch(() => undefined);\n }\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub)\n ? pubsub\n : ((pubsub as PubSub & { lease?: LeaseProvider }).lease ?? NoopLeaseProvider);\n}\n"],"mappings":";;;;AASA,MAAa,sCAAsC,IAAI;AACvD,MAAM,mBAAmB;AAQzB,IAAa,uBAAb,cAA0C,aAAa;CACrD;CAEA;CACA;CACA;CACA;CACA;CACA,cAAuB,WAAW;CAElC,WAAW;CACX;CACA;CACA,iBAAgC;CAEhC,YAAY,QAAoC;EAC9C,MAAM;EACN,KAAKA,iBAAiB,OAAO;EAC7B,KAAK,OAAO,GAAG,OAAO,cAAc;EACpC,KAAKI,YAAY,GAAG,OAAO,cAAc;EACzC,KAAKH,aAAa,OAAO;EACzB,KAAKC,cAAc,OAAO,cAAA;EAC1B,IAAI,CAAC,OAAO,SAAS,KAAKA,WAAW,KAAK,KAAKA,eAAe,GAC5D,MAAM,IAAI,MAAM,GAAG,OAAO,cAAc,qDAAqD;EAE/F,KAAKC,cAAc,KAAK,IAAI,kBAAkB,KAAKD,cAAc,CAAC;CACpE;CAEA,MAAM,KAAK,MAAiC;EAC1C,MAAM,MAAM,KAAK,IAAI;EACrB,KAAKI,iBAAiB,iBAAiB,KAAK,MAAM;CACpD;CAEA,MAAM,QAAuB;EAC3B,IAAI,KAAKC,UAAU;EACnB,IAAI,CAAC,KAAK,MAAM,MAAM,IAAI,MAAM,kDAAkD;EAClF,KAAKA,WAAW;EAChB,KAAK,KAAK,OAAO,KAAK,GAAG,KAAKP,eAAe,kCAAkC,EAAE,YAAY,KAAKE,YAAY,CAAC;EAC/G,KAAKM,UAAU,CAAC;CAClB;CAEA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAKD,UAAU;EACpB,KAAKA,WAAW;EAChB,IAAI,KAAKE,QAAQ,aAAa,KAAKA,MAAM;EACzC,KAAKA,SAAS,KAAA;EACd,MAAM,KAAKC;CACb;CAEA,IAAI,YAAqB;EACvB,OAAO,KAAKH;CACd;CAEA,UAAU,SAAuB;EAC/B,IAAI,CAAC,KAAKA,UAAU;EACpB,KAAKE,SAAS,iBAAiB;GAC7B,KAAKA,SAAS,KAAA;GACd,MAAM,MAAM,KAAKE,MAAM,CAAC,CAAC,cAAc;IACrC,KAAKD,YAAY,KAAA;IACjB,KAAKF,UAAU,KAAKN,WAAW;GACjC,CAAC;GACD,KAAKQ,YAAY;EACnB,GAAG,OAAO;EACV,KAAKD,OAAO,QAAQ;CACtB;CAEA,MAAME,QAAuB;EAI3B,IAAI,EAAC,MAHe,KAAKL,eACtB,aAAa,KAAKF,WAAW,KAAKC,aAAa,KAAKF,WAAW,CAAC,CAChE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAA,CACzB,UAAU;EAKrB,MAAM,eAAe,kBACb;GACJ,KAAUG,eACP,WAAW,KAAKF,WAAW,KAAKC,aAAa,KAAKF,WAAW,CAAC,CAC9D,OAAM,UAAS;IACd,KAAK,MAAM,OAAO,KAAK,GAAG,KAAKH,eAAe,wCAAwC,EACpF,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;GACH,CAAC;EACL,GACA,KAAK,IAAI,KAAO,KAAK,MAAM,KAAKG,cAAc,CAAC,CAAC,CAClD;EACA,aAAa,QAAQ;EAErB,IAAI;GACF,MAAM,YAAY,KAAK,IAAI;GAC3B,MAAM,EAAE,QAAQ,GAAG,WAAW,MAAM,KAAKF,WAAW;GACpD,MAAM,UAAU;IAAE,GAAG;IAAQ,YAAY,KAAK,IAAI,IAAI;GAAU;GAChE,IAAI,OAAO,SAAS,GAClB,KAAK,MAAM,OAAO,KAAK,GAAG,KAAKD,eAAe,iDAAiD;IAC7F,GAAG;IACH;GACF,CAAC;QAED,KAAK,MAAM,OAAO,MAAM,GAAG,KAAKA,eAAe,mCAAmC,OAAO;EAE7F,SAAS,OAAO;GACd,KAAK,MAAM,OAAO,KAAK,GAAG,KAAKA,eAAe,gCAAgC,EAC5E,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;EACH,UAAU;GACR,cAAc,YAAY;GAC1B,MAAM,KAAKM,eAAe,aAAa,KAAKF,WAAW,KAAKC,WAAW,CAAC,CAAC,YAAY,KAAA,CAAS;EAChG;CACF;AACF;AAEA,SAAS,iBAAiB,QAA+B;CACvD,MAAM,cAAe,OAA2E;CAChG,IAAI,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,MAAM,KAAK;CAC1E,OAAO,gBAAgB,MAAM,IACzB,SACE,OAA8C,SAAS;AAC/D"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Intake, IntakeIssue } from '../capabilities/intake.js';
|
|
2
|
+
import type { FactoryProject, FactoryProjectsStorage } from '../storage/domains/projects/base.js';
|
|
3
|
+
import type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';
|
|
4
|
+
export interface IssueReconcileSummary {
|
|
5
|
+
projects: number;
|
|
6
|
+
checked: number;
|
|
7
|
+
updated: number;
|
|
8
|
+
closed: number;
|
|
9
|
+
missing: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
errors: Array<{
|
|
12
|
+
projectId: string;
|
|
13
|
+
workItemId?: string;
|
|
14
|
+
error: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Scope passed in by the caller. Providers that reconcile per-repository (like
|
|
19
|
+
* GitHub, sharing target discovery with the PR reconciler) pass a target set
|
|
20
|
+
* and a `matches(item, scope)` predicate. Providers with no repository concept
|
|
21
|
+
* (Linear) pass no scope and every issue item is considered.
|
|
22
|
+
*/
|
|
23
|
+
export interface IssueReconcileScope<TScope> {
|
|
24
|
+
scopes: TScope[];
|
|
25
|
+
matches(item: WorkItemRow, scope: TScope): boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface IssueReconcilerOptions<TScope = void> {
|
|
28
|
+
integrationId: string;
|
|
29
|
+
intake: Intake;
|
|
30
|
+
projects: Pick<FactoryProjectsStorage, 'listAll'>;
|
|
31
|
+
storage: WorkItemsStorage;
|
|
32
|
+
externalSource?(item: WorkItemRow): {
|
|
33
|
+
type: string;
|
|
34
|
+
externalId: string;
|
|
35
|
+
};
|
|
36
|
+
issueId(item: WorkItemRow): string | undefined;
|
|
37
|
+
metadata(item: WorkItemRow, issue: IntakeIssue): Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Called when a closed issue (stateType 'completed' or 'canceled') is detected
|
|
40
|
+
* on a non-terminal work item. Use this to replay the close event through
|
|
41
|
+
* the provider's rules ingress.
|
|
42
|
+
*/
|
|
43
|
+
onClosed?(item: WorkItemRow, issue: IntakeIssue, project: FactoryProject): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export type IssueReconciler<TScope = void> = TScope extends void ? () => Promise<IssueReconcileSummary> : (scope: IssueReconcileScope<TScope>) => Promise<IssueReconcileSummary>;
|
|
46
|
+
export declare function createIssueReconciler<TScope = void>(options: IssueReconcilerOptions<TScope>): IssueReconciler<TScope>;
|
|
47
|
+
//# sourceMappingURL=issue-reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-reconciler.d.ts","sourceRoot":"","sources":["../../src/integrations/issue-reconciler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClG,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1E;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB,CAAC,MAAM;IACzC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACpD;AAED,MAAM,WAAW,sBAAsB,CAAC,MAAM,GAAG,IAAI;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC;IAClD,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,CAAC,CAAC,IAAI,EAAE,WAAW,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzE;;;;OAIG;IACH,QAAQ,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1F;AAED,MAAM,MAAM,eAAe,CAAC,MAAM,GAAG,IAAI,IAAI,MAAM,SAAS,IAAI,GAC5D,MAAM,OAAO,CAAC,qBAAqB,CAAC,GACpC,CAAC,KAAK,EAAE,mBAAmB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAsC3E,wBAAgB,qBAAqB,CAAC,MAAM,GAAG,IAAI,EACjD,OAAO,EAAE,sBAAsB,CAAC,MAAM,CAAC,GACtC,eAAe,CAAC,MAAM,CAAC,CA8EzB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//#region src/integrations/issue-reconciler.ts
|
|
2
|
+
function sameValue(left, right) {
|
|
3
|
+
if (Array.isArray(right)) {
|
|
4
|
+
if (!Array.isArray(left)) return right.length === 0;
|
|
5
|
+
const leftStrings = left.filter((value) => typeof value === "string").slice().sort();
|
|
6
|
+
const rightStrings = right.filter((value) => typeof value === "string").slice().sort();
|
|
7
|
+
return leftStrings.length === rightStrings.length && leftStrings.every((value, index) => value === rightStrings[index]);
|
|
8
|
+
}
|
|
9
|
+
return left === right;
|
|
10
|
+
}
|
|
11
|
+
function metadataMatches(current, desired) {
|
|
12
|
+
return Object.entries(desired).every(([key, value]) => sameValue(current?.[key], value));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Drop `undefined` entries so we never spread them into stored metadata. An
|
|
16
|
+
* `undefined` field on the desired patch means "no signal from the live issue"
|
|
17
|
+
* — we must preserve whatever was already stored, not clobber it.
|
|
18
|
+
*/
|
|
19
|
+
function withoutUndefined(record) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [key, value] of Object.entries(record)) if (value !== void 0) out[key] = value;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function issueItems(project, items, integrationId) {
|
|
25
|
+
return items.filter((item) => item.factoryProjectId === project.id && item.externalSource?.integrationId === integrationId && item.externalSource.type === "issue");
|
|
26
|
+
}
|
|
27
|
+
function createIssueReconciler(options) {
|
|
28
|
+
const run = async (scope) => {
|
|
29
|
+
const summary = {
|
|
30
|
+
projects: 0,
|
|
31
|
+
checked: 0,
|
|
32
|
+
updated: 0,
|
|
33
|
+
closed: 0,
|
|
34
|
+
missing: 0,
|
|
35
|
+
failed: 0,
|
|
36
|
+
errors: []
|
|
37
|
+
};
|
|
38
|
+
if (scope && scope.scopes.length === 0) return summary;
|
|
39
|
+
const projects = await options.projects.listAll();
|
|
40
|
+
for (const project of projects) {
|
|
41
|
+
const candidates = issueItems(project, await options.storage.list({
|
|
42
|
+
orgId: project.orgId,
|
|
43
|
+
factoryProjectId: project.id
|
|
44
|
+
}), options.integrationId);
|
|
45
|
+
const items = scope ? candidates.filter((item) => scope.scopes.some((target) => scope.matches(item, target))) : candidates;
|
|
46
|
+
if (items.length === 0) continue;
|
|
47
|
+
summary.projects += 1;
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
summary.checked += 1;
|
|
50
|
+
try {
|
|
51
|
+
const resolved = await options.intake.resolveIntakeDispatch?.({
|
|
52
|
+
orgId: project.orgId,
|
|
53
|
+
externalSource: options.externalSource?.(item) ?? item.externalSource
|
|
54
|
+
});
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
summary.missing += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const issueId = options.issueId(item) ?? resolved.issueId;
|
|
60
|
+
const issue = await options.intake.getIssue({
|
|
61
|
+
...resolved,
|
|
62
|
+
issueId
|
|
63
|
+
});
|
|
64
|
+
if (!issue) {
|
|
65
|
+
summary.missing += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const isClosed = issue.stateType === "completed" || issue.stateType === "canceled";
|
|
69
|
+
const stage = item.stages[0];
|
|
70
|
+
if (isClosed && !(stage === "done" || stage === "canceled") && options.onClosed) {
|
|
71
|
+
await options.onClosed(item, issue, project);
|
|
72
|
+
summary.closed += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const metadata = withoutUndefined(options.metadata(item, issue));
|
|
76
|
+
if (metadataMatches(item.metadata, metadata)) continue;
|
|
77
|
+
await options.storage.update({
|
|
78
|
+
orgId: project.orgId,
|
|
79
|
+
id: item.id,
|
|
80
|
+
userId: "factory-rule-dispatcher",
|
|
81
|
+
patch: { metadata: {
|
|
82
|
+
...item.metadata ?? {},
|
|
83
|
+
...metadata
|
|
84
|
+
} }
|
|
85
|
+
});
|
|
86
|
+
summary.updated += 1;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
summary.failed += 1;
|
|
89
|
+
summary.errors.push({
|
|
90
|
+
projectId: project.id,
|
|
91
|
+
workItemId: item.id,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return summary;
|
|
98
|
+
};
|
|
99
|
+
return run;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { createIssueReconciler };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=issue-reconciler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-reconciler.js","names":[],"sources":["../../src/integrations/issue-reconciler.ts"],"sourcesContent":["import type { Intake, IntakeIssue } from '../capabilities/intake.js';\nimport type { FactoryProject, FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\nexport interface IssueReconcileSummary {\n projects: number;\n checked: number;\n updated: number;\n closed: number;\n missing: number;\n failed: number;\n errors: Array<{ projectId: string; workItemId?: string; error: string }>;\n}\n\n/**\n * Scope passed in by the caller. Providers that reconcile per-repository (like\n * GitHub, sharing target discovery with the PR reconciler) pass a target set\n * and a `matches(item, scope)` predicate. Providers with no repository concept\n * (Linear) pass no scope and every issue item is considered.\n */\nexport interface IssueReconcileScope<TScope> {\n scopes: TScope[];\n matches(item: WorkItemRow, scope: TScope): boolean;\n}\n\nexport interface IssueReconcilerOptions<TScope = void> {\n integrationId: string;\n intake: Intake;\n projects: Pick<FactoryProjectsStorage, 'listAll'>;\n storage: WorkItemsStorage;\n externalSource?(item: WorkItemRow): { type: string; externalId: string };\n issueId(item: WorkItemRow): string | undefined;\n metadata(item: WorkItemRow, issue: IntakeIssue): Record<string, unknown>;\n /**\n * Called when a closed issue (stateType 'completed' or 'canceled') is detected\n * on a non-terminal work item. Use this to replay the close event through\n * the provider's rules ingress.\n */\n onClosed?(item: WorkItemRow, issue: IntakeIssue, project: FactoryProject): Promise<void>;\n}\n\nexport type IssueReconciler<TScope = void> = TScope extends void\n ? () => Promise<IssueReconcileSummary>\n : (scope: IssueReconcileScope<TScope>) => Promise<IssueReconcileSummary>;\n\nfunction sameValue(left: unknown, right: unknown): boolean {\n if (Array.isArray(right)) {\n if (!Array.isArray(left)) return right.length === 0;\n const leftStrings = left.filter((value): value is string => typeof value === 'string').slice().sort();\n const rightStrings = right.filter((value): value is string => typeof value === 'string').slice().sort();\n return leftStrings.length === rightStrings.length && leftStrings.every((value, index) => value === rightStrings[index]);\n }\n return left === right;\n}\n\nfunction metadataMatches(current: Record<string, unknown> | null, desired: Record<string, unknown>): boolean {\n return Object.entries(desired).every(([key, value]) => sameValue(current?.[key], value));\n}\n\n/**\n * Drop `undefined` entries so we never spread them into stored metadata. An\n * `undefined` field on the desired patch means \"no signal from the live issue\"\n * — we must preserve whatever was already stored, not clobber it.\n */\nfunction withoutUndefined(record: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(record)) {\n if (value !== undefined) out[key] = value;\n }\n return out;\n}\n\nfunction issueItems(project: FactoryProject, items: WorkItemRow[], integrationId: string): WorkItemRow[] {\n return items.filter(\n item =>\n item.factoryProjectId === project.id &&\n item.externalSource?.integrationId === integrationId &&\n item.externalSource.type === 'issue',\n );\n}\n\nexport function createIssueReconciler<TScope = void>(\n options: IssueReconcilerOptions<TScope>,\n): IssueReconciler<TScope> {\n const run = async (scope?: IssueReconcileScope<TScope>): Promise<IssueReconcileSummary> => {\n const summary: IssueReconcileSummary = {\n projects: 0,\n checked: 0,\n updated: 0,\n closed: 0,\n missing: 0,\n failed: 0,\n errors: [],\n };\n\n // A scoped sweep with zero targets is a no-op: the caller is telling us\n // there are no configured repositories to consider.\n if (scope && scope.scopes.length === 0) return summary;\n\n const projects = await options.projects.listAll();\n for (const project of projects) {\n const all = await options.storage.list({ orgId: project.orgId, factoryProjectId: project.id });\n const candidates = issueItems(project, all, options.integrationId);\n const items = scope\n ? candidates.filter(item => scope.scopes.some(target => scope.matches(item, target)))\n : candidates;\n if (items.length === 0) continue;\n summary.projects += 1;\n\n for (const item of items) {\n summary.checked += 1;\n try {\n const resolved = await options.intake.resolveIntakeDispatch?.({\n orgId: project.orgId,\n externalSource: options.externalSource?.(item) ?? item.externalSource!,\n });\n if (!resolved) {\n summary.missing += 1;\n continue;\n }\n const issueId = options.issueId(item) ?? resolved.issueId;\n const issue = await options.intake.getIssue({ ...resolved, issueId });\n if (!issue) {\n summary.missing += 1;\n continue;\n }\n\n // Close detection for Linear: replay through rules ingress if closed\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n const stage = item.stages[0];\n const isTerminal = stage === 'done' || stage === 'canceled';\n if (isClosed && !isTerminal && options.onClosed) {\n await options.onClosed(item, issue, project);\n summary.closed += 1;\n continue; // Skip metadata patch for closed issues\n }\n\n const metadata = withoutUndefined(options.metadata(item, issue));\n if (metadataMatches(item.metadata, metadata)) continue;\n await options.storage.update({\n orgId: project.orgId,\n id: item.id,\n userId: 'factory-rule-dispatcher',\n patch: { metadata: { ...(item.metadata ?? {}), ...metadata } },\n });\n summary.updated += 1;\n } catch (error) {\n summary.failed += 1;\n summary.errors.push({\n projectId: project.id,\n workItemId: item.id,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n return summary;\n };\n\n return run as IssueReconciler<TScope>;\n}\n"],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { RequestContext } from '@mastra/core/request-context';
|
|
22
22
|
import type { ApiRoute } from '@mastra/core/server';
|
|
23
|
+
import type { MastraWorker } from '@mastra/core/worker';
|
|
23
24
|
import type { Intake } from '../../capabilities/intake.js';
|
|
24
25
|
import type { RouteAuth } from '../../routes/route.js';
|
|
25
26
|
import type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';
|
|
@@ -64,6 +65,8 @@ export interface LinearIssue {
|
|
|
64
65
|
stateType: string;
|
|
65
66
|
priorityLabel: string;
|
|
66
67
|
assignee: string | null;
|
|
68
|
+
/** Display name of the Linear user who created the issue, when Linear returns one. */
|
|
69
|
+
creator: string | null;
|
|
67
70
|
team: string | null;
|
|
68
71
|
labels: string[];
|
|
69
72
|
createdAt: string;
|
|
@@ -214,6 +217,7 @@ export declare class LinearIntegration implements FactoryIntegration {
|
|
|
214
217
|
* issue doesn't exist.
|
|
215
218
|
*/
|
|
216
219
|
createIssueComment(accessToken: string, idOrIdentifier: string, body: string): Promise<LinearCreatedComment | null>;
|
|
220
|
+
workers(ctx: IntegrationContext): MastraWorker[];
|
|
217
221
|
/**
|
|
218
222
|
* The integration's HTTP surface: `/web/linear/*` + `/auth/linear/*` Mastra
|
|
219
223
|
* `apiRoutes` (status, OAuth connect/callback, projects + issues for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/integration.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IssueReconcileWorker } from "../issue-reconcile-worker.js";
|
|
2
2
|
import { attachLinearRules } from "./rules.js";
|
|
3
|
+
import { attachLinearIssueReconciler } from "./issue-reconciler.js";
|
|
4
|
+
import { buildLinearRoutes } from "./routes.js";
|
|
3
5
|
import { buildLinearAgentTools } from "./agent-tools.js";
|
|
4
6
|
//#region src/integrations/linear/integration.ts
|
|
5
7
|
const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
|
|
@@ -474,6 +476,7 @@ var LinearIntegration = class {
|
|
|
474
476
|
state { name type }
|
|
475
477
|
project { id }
|
|
476
478
|
assignee { name }
|
|
479
|
+
creator { name }
|
|
477
480
|
team { key }
|
|
478
481
|
labels { nodes { name } }
|
|
479
482
|
}
|
|
@@ -496,6 +499,7 @@ var LinearIntegration = class {
|
|
|
496
499
|
stateType: node.state.type,
|
|
497
500
|
priorityLabel: node.priorityLabel,
|
|
498
501
|
assignee: node.assignee?.name ?? null,
|
|
502
|
+
creator: node.creator?.name ?? null,
|
|
499
503
|
team: node.team?.key ?? null,
|
|
500
504
|
labels: node.labels.nodes.map((label) => label.name),
|
|
501
505
|
createdAt: node.createdAt,
|
|
@@ -549,6 +553,7 @@ var LinearIntegration = class {
|
|
|
549
553
|
state { name type }
|
|
550
554
|
project { id }
|
|
551
555
|
assignee { name }
|
|
556
|
+
creator { name }
|
|
552
557
|
team { key }
|
|
553
558
|
labels { nodes { name } }
|
|
554
559
|
comments(first: $commentsFirst) {
|
|
@@ -578,6 +583,7 @@ var LinearIntegration = class {
|
|
|
578
583
|
stateType: issue.state.type,
|
|
579
584
|
priorityLabel: issue.priorityLabel,
|
|
580
585
|
assignee: issue.assignee?.name ?? null,
|
|
586
|
+
creator: issue.creator?.name ?? null,
|
|
581
587
|
team: issue.team?.key ?? null,
|
|
582
588
|
labels: issue.labels.nodes.map((label) => label.name),
|
|
583
589
|
createdAt: issue.createdAt,
|
|
@@ -614,6 +620,17 @@ var LinearIntegration = class {
|
|
|
614
620
|
if (!data.commentCreate.success || !data.commentCreate.comment) throw new Error("Linear did not accept the comment.");
|
|
615
621
|
return data.commentCreate.comment;
|
|
616
622
|
}
|
|
623
|
+
workers(ctx) {
|
|
624
|
+
if (process.env.MASTRACODE_LINEAR_RECONCILE_ENABLED?.trim().toLowerCase() === "false") return [];
|
|
625
|
+
const reconcile = attachLinearIssueReconciler(this, ctx);
|
|
626
|
+
if (!reconcile) return [];
|
|
627
|
+
const intervalMs = Number(process.env.MASTRACODE_LINEAR_RECONCILE_INTERVAL_MS);
|
|
628
|
+
return [new IssueReconcileWorker({
|
|
629
|
+
integrationId: this.id,
|
|
630
|
+
reconcile,
|
|
631
|
+
...Number.isSafeInteger(intervalMs) && intervalMs > 0 ? { intervalMs } : {}
|
|
632
|
+
})];
|
|
633
|
+
}
|
|
617
634
|
/**
|
|
618
635
|
* The integration's HTTP surface: `/web/linear/*` + `/auth/linear/*` Mastra
|
|
619
636
|
* `apiRoutes` (status, OAuth connect/callback, projects + issues for
|
|
@@ -654,7 +671,7 @@ function linearIssueToIntakeIssue(issue) {
|
|
|
654
671
|
identifier: issue.identifier,
|
|
655
672
|
title: issue.title,
|
|
656
673
|
url: issue.url,
|
|
657
|
-
author:
|
|
674
|
+
author: issue.creator,
|
|
658
675
|
state: issue.state,
|
|
659
676
|
stateType: issue.stateType,
|
|
660
677
|
priority: issue.priorityLabel,
|