@mastra/factory 0.2.1-alpha.2 → 0.2.1-alpha.4

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +3462 -3583
  4. package/dist/factory.js.map +1 -1
  5. package/dist/index.js +3462 -3583
  6. package/dist/index.js.map +1 -1
  7. package/dist/integrations/base.d.ts +13 -25
  8. package/dist/integrations/base.d.ts.map +1 -1
  9. package/dist/integrations/github/integration.d.ts.map +1 -1
  10. package/dist/integrations/github/integration.js +625 -217
  11. package/dist/integrations/github/integration.js.map +1 -1
  12. package/dist/integrations/github/routes.d.ts +1 -4
  13. package/dist/integrations/github/routes.d.ts.map +1 -1
  14. package/dist/integrations/github/routes.js +113 -203
  15. package/dist/integrations/github/routes.js.map +1 -1
  16. package/dist/integrations/github/rules.d.ts +30 -0
  17. package/dist/integrations/github/rules.d.ts.map +1 -0
  18. package/dist/{rules/github-service.js → integrations/github/rules.js} +27 -8
  19. package/dist/integrations/github/rules.js.map +1 -0
  20. package/dist/integrations/linear/agent-tools.js.map +1 -1
  21. package/dist/integrations/linear/integration.d.ts.map +1 -1
  22. package/dist/integrations/linear/integration.js +352 -3
  23. package/dist/integrations/linear/integration.js.map +1 -1
  24. package/dist/integrations/linear/routes.d.ts +2 -2
  25. package/dist/integrations/linear/routes.d.ts.map +1 -1
  26. package/dist/integrations/linear/routes.js +2 -2
  27. package/dist/integrations/linear/routes.js.map +1 -1
  28. package/dist/integrations/linear/rules.d.ts +42 -0
  29. package/dist/integrations/linear/rules.d.ts.map +1 -0
  30. package/dist/{rules/linear-service.js → integrations/linear/rules.js} +14 -4
  31. package/dist/integrations/linear/rules.js.map +1 -0
  32. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  33. package/dist/integrations/platform/github/integration.js +626 -218
  34. package/dist/integrations/platform/github/integration.js.map +1 -1
  35. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  36. package/dist/integrations/platform/linear/integration.js +353 -3
  37. package/dist/integrations/platform/linear/integration.js.map +1 -1
  38. package/dist/routes/oauth.js +6 -39
  39. package/dist/routes/oauth.js.map +1 -1
  40. package/dist/routes/surface.d.ts +3 -1
  41. package/dist/routes/surface.d.ts.map +1 -1
  42. package/dist/routes/surface.js +230 -619
  43. package/dist/routes/surface.js.map +1 -1
  44. package/dist/routes/work-items.js.map +1 -1
  45. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  46. package/dist/storage/domains/work-items/base.js +74 -75
  47. package/dist/storage/domains/work-items/base.js.map +1 -1
  48. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  49. package/package.json +5 -5
  50. package/dist/integrations/github/project-lock.d.ts +0 -100
  51. package/dist/integrations/github/project-lock.d.ts.map +0 -1
  52. package/dist/integrations/github/project-lock.js +0 -103
  53. package/dist/integrations/github/project-lock.js.map +0 -1
  54. package/dist/rules/github-service.d.ts +0 -29
  55. package/dist/rules/github-service.d.ts.map +0 -1
  56. package/dist/rules/github-service.js.map +0 -1
  57. package/dist/rules/linear-service.d.ts +0 -27
  58. package/dist/rules/linear-service.d.ts.map +0 -1
  59. package/dist/rules/linear-service.js.map +0 -1
@@ -93,95 +93,8 @@ async function clearGithubPat(storage, orgId, kind = "default") {
93
93
  await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);
94
94
  }
95
95
 
96
- // src/integrations/github/project-lock.ts
97
- import { createHash } from "crypto";
98
- var inProcessLocks = /* @__PURE__ */ new Map();
99
- var DEFAULT_PROJECT_LOCK_TIMEOUT_MS = 6e4;
100
- var ProjectLockTimeoutError = class extends Error {
101
- key;
102
- timeoutMs;
103
- constructor(key, timeoutMs) {
104
- super(`Project lock critical section for "${key}" exceeded ${timeoutMs}ms`);
105
- this.name = "ProjectLockTimeoutError";
106
- this.key = key;
107
- this.timeoutMs = timeoutMs;
108
- }
109
- };
110
- async function runWithTimeout(key, timeoutMs, fn) {
111
- const controller = new AbortController();
112
- const timer = setTimeout(() => controller.abort(), timeoutMs);
113
- const abortError = new Promise((_, reject) => {
114
- controller.signal.addEventListener("abort", () => reject(new ProjectLockTimeoutError(key, timeoutMs)), {
115
- once: true
116
- });
117
- });
118
- const work = fn(controller.signal);
119
- work.catch(() => {
120
- });
121
- try {
122
- return await Promise.race([work, abortError]);
123
- } finally {
124
- clearTimeout(timer);
125
- }
126
- }
127
- function hashKey(key) {
128
- const digest = createHash("sha256").update(key).digest();
129
- const a = digest.readInt32BE(0);
130
- const b = digest.readInt32BE(4);
131
- return [a, b];
132
- }
133
- function withProjectLock(options) {
134
- const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
135
- const prev = inProcessLocks.get(key) ?? Promise.resolve();
136
- const run = () => withDbAdvisoryLock({ key, storage, fn, pool, timeoutMs });
137
- const next = prev.then(run, run);
138
- const tail = next.then(
139
- () => void 0,
140
- () => void 0
141
- );
142
- inProcessLocks.set(key, tail);
143
- void tail.then(() => {
144
- if (inProcessLocks.get(key) === tail) {
145
- inProcessLocks.delete(key);
146
- }
147
- });
148
- return next;
149
- }
150
- async function withDbAdvisoryLock(options) {
151
- const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
152
- if (process.env.MASTRACODE_DISTRIBUTED_LOCK === "0") {
153
- return runWithTimeout(key, timeoutMs, fn);
154
- }
155
- if (pool) return advisoryLockOver(pool, key, timeoutMs, fn);
156
- if (typeof storage?.withDistributedLock !== "function") {
157
- return runWithTimeout(key, timeoutMs, fn);
158
- }
159
- return storage.withDistributedLock(key, () => runWithTimeout(key, timeoutMs, fn));
160
- }
161
- async function advisoryLockOver(pool, key, timeoutMs, fn) {
162
- const [k1, k2] = hashKey(key);
163
- const client = await pool.connect();
164
- try {
165
- await client.query("BEGIN");
166
- await client.query("SELECT pg_advisory_xact_lock($1, $2)", [k1, k2]);
167
- try {
168
- const result = await runWithTimeout(key, timeoutMs, fn);
169
- await client.query("COMMIT");
170
- return result;
171
- } catch (err) {
172
- try {
173
- await client.query("ROLLBACK");
174
- } catch {
175
- }
176
- throw err;
177
- }
178
- } finally {
179
- client.release();
180
- }
181
- }
182
-
183
96
  // src/integrations/github/sandbox.ts
184
- import { createHash as createHash2 } from "crypto";
97
+ import { createHash } from "crypto";
185
98
  function bindingStore(row, storage) {
186
99
  return {
187
100
  sandboxId: row.sandboxId,
@@ -377,7 +290,7 @@ var WorktreeError = class extends Error {
377
290
  function safeBranchDir(branch) {
378
291
  const sanitized = branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\/+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100) || "work";
379
292
  if (sanitized === branch) return sanitized;
380
- const hash = createHash2("sha256").update(branch).digest("hex").slice(0, 8);
293
+ const hash = createHash("sha256").update(branch).digest("hex").slice(0, 8);
381
294
  return `${sanitized}-${hash}`;
382
295
  }
383
296
  function computeWorktreePath(repoWorkdir, branch) {
@@ -799,6 +712,20 @@ async function handleGithubWebhook(c, options) {
799
712
  }
800
713
 
801
714
  // src/integrations/github/routes.ts
715
+ var sessionOperationLocks = /* @__PURE__ */ new Map();
716
+ function withSessionOperationLock(sessionId, fn) {
717
+ const previous = sessionOperationLocks.get(sessionId) ?? Promise.resolve();
718
+ const next = previous.then(fn, fn);
719
+ const tail = next.then(
720
+ () => void 0,
721
+ () => void 0
722
+ );
723
+ sessionOperationLocks.set(sessionId, tail);
724
+ void tail.then(() => {
725
+ if (sessionOperationLocks.get(sessionId) === tail) sessionOperationLocks.delete(sessionId);
726
+ });
727
+ return next;
728
+ }
802
729
  function loose(c) {
803
730
  return c;
804
731
  }
@@ -818,8 +745,8 @@ function pullRequestNumberFromUrl(value, expectedRepo) {
818
745
  if (url.protocol !== "https:" || url.hostname !== "github.com" || match?.[1]?.toLowerCase() !== expectedRepo.toLowerCase()) {
819
746
  return void 0;
820
747
  }
821
- const number = Number(match[2]);
822
- return Number.isInteger(number) && number > 0 ? number : void 0;
748
+ const number2 = Number(match[2]);
749
+ return Number.isInteger(number2) && number2 > 0 ? number2 : void 0;
823
750
  } catch {
824
751
  return void 0;
825
752
  }
@@ -1483,7 +1410,7 @@ function buildGithubRoutes(options) {
1483
1410
  }
1484
1411
  })
1485
1412
  );
1486
- routes.push(...buildProjectGitRoutes({ github, auth, fleet, storage, emitAudit }));
1413
+ routes.push(...buildProjectGitRoutes({ github, auth, fleet, emitAudit }));
1487
1414
  return routes;
1488
1415
  }
1489
1416
  async function loadOrgProject(options) {
@@ -1600,7 +1527,6 @@ function buildProjectGitRoutes({
1600
1527
  github,
1601
1528
  auth,
1602
1529
  fleet,
1603
- storage,
1604
1530
  emitAudit
1605
1531
  }) {
1606
1532
  return [
@@ -1721,31 +1647,27 @@ function buildProjectGitRoutes({
1721
1647
  }
1722
1648
  const { workdir, sandboxBinding } = sessionWorkspace;
1723
1649
  try {
1724
- return await withProjectLock({
1725
- key: `${project.id}:${userId}`,
1726
- storage,
1727
- fn: async () => {
1728
- const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1729
- const result = await commitAll(
1730
- sandbox,
1731
- workdir,
1732
- body.message,
1733
- identityFromUser(await auth.ensureUser(loose(c)))
1734
- );
1735
- if (result.committed) {
1736
- await emitAudit?.({
1737
- context: loose(c),
1738
- input: {
1739
- action: "factory.git.commit",
1740
- factoryProjectId: project.factoryProjectId,
1741
- projectRepositoryId: project.id,
1742
- targets: [{ type: "session", id: sessionWorkspace.session.sessionId }],
1743
- metadata: { sessionId: sessionWorkspace.session.sessionId }
1744
- }
1745
- });
1746
- }
1747
- return c.json({ committed: result.committed });
1650
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1651
+ const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1652
+ const result = await commitAll(
1653
+ sandbox,
1654
+ workdir,
1655
+ body.message,
1656
+ identityFromUser(await auth.ensureUser(loose(c)))
1657
+ );
1658
+ if (result.committed) {
1659
+ await emitAudit?.({
1660
+ context: loose(c),
1661
+ input: {
1662
+ action: "factory.git.commit",
1663
+ factoryProjectId: project.factoryProjectId,
1664
+ projectRepositoryId: project.id,
1665
+ targets: [{ type: "session", id: sessionWorkspace.session.sessionId }],
1666
+ metadata: { sessionId: sessionWorkspace.session.sessionId }
1667
+ }
1668
+ });
1748
1669
  }
1670
+ return c.json({ committed: result.committed });
1749
1671
  });
1750
1672
  } catch (err) {
1751
1673
  return gitErrorResponse(loose(c), err);
@@ -1776,29 +1698,25 @@ function buildProjectGitRoutes({
1776
1698
  }
1777
1699
  const { workdir, sandboxBinding } = sessionWorkspace;
1778
1700
  try {
1779
- return await withProjectLock({
1780
- key: `${project.id}:${userId}`,
1781
- storage,
1782
- fn: async () => {
1783
- const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1784
- const access = await github.versionControl.getRepositoryAccess({
1785
- orgId,
1786
- repositoryId: project.repository.id
1787
- });
1788
- if (!access.authorization) throw new Error("Repository access did not include a bearer token.");
1789
- await pushBranch(sandbox, workdir, branch, access.authorization.token, project.repository.slug);
1790
- await emitAudit?.({
1791
- context: loose(c),
1792
- input: {
1793
- action: "factory.git.push",
1794
- factoryProjectId: project.factoryProjectId,
1795
- projectRepositoryId: project.id,
1796
- targets: [{ type: "branch", id: branch }],
1797
- metadata: { branch, sessionId: sessionWorkspace.session.sessionId }
1798
- }
1799
- });
1800
- return c.json({ pushed: true, branch });
1801
- }
1701
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1702
+ const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1703
+ const access = await github.versionControl.getRepositoryAccess({
1704
+ orgId,
1705
+ repositoryId: project.repository.id
1706
+ });
1707
+ if (!access.authorization) throw new Error("Repository access did not include a bearer token.");
1708
+ await pushBranch(sandbox, workdir, branch, access.authorization.token, project.repository.slug);
1709
+ await emitAudit?.({
1710
+ context: loose(c),
1711
+ input: {
1712
+ action: "factory.git.push",
1713
+ factoryProjectId: project.factoryProjectId,
1714
+ projectRepositoryId: project.id,
1715
+ targets: [{ type: "branch", id: branch }],
1716
+ metadata: { branch, sessionId: sessionWorkspace.session.sessionId }
1717
+ }
1718
+ });
1719
+ return c.json({ pushed: true, branch });
1802
1720
  });
1803
1721
  } catch (err) {
1804
1722
  return gitErrorResponse(loose(c), err);
@@ -1840,60 +1758,56 @@ function buildProjectGitRoutes({
1840
1758
  return c.json({ error: "Invalid sessionId" }, 400);
1841
1759
  }
1842
1760
  try {
1843
- return await withProjectLock({
1844
- key: `${project.id}:${userId}`,
1845
- storage,
1846
- fn: async () => {
1847
- const result = await github.versionControl.createPullRequest({
1848
- connection: {
1849
- type: "app-installation",
1850
- installationId: Number(project.installation.externalId)
1851
- },
1852
- sourceId: project.repository.slug,
1853
- baseBranch: base,
1854
- headBranch: head,
1855
- title,
1856
- body: prBody,
1857
- actingUserId: userId
1858
- });
1859
- await emitAudit?.({
1860
- context: loose(c),
1861
- input: {
1862
- action: "factory.git.pr_opened",
1863
- factoryProjectId: project.factoryProjectId,
1761
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1762
+ const result = await github.versionControl.createPullRequest({
1763
+ connection: {
1764
+ type: "app-installation",
1765
+ installationId: Number(project.installation.externalId)
1766
+ },
1767
+ sourceId: project.repository.slug,
1768
+ baseBranch: base,
1769
+ headBranch: head,
1770
+ title,
1771
+ body: prBody,
1772
+ actingUserId: userId
1773
+ });
1774
+ await emitAudit?.({
1775
+ context: loose(c),
1776
+ input: {
1777
+ action: "factory.git.pr_opened",
1778
+ factoryProjectId: project.factoryProjectId,
1779
+ projectRepositoryId: project.id,
1780
+ targets: [{ type: "pull_request", id: result.url, name: title }],
1781
+ metadata: { branch: head, base, url: result.url }
1782
+ }
1783
+ });
1784
+ const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
1785
+ if (pullRequestNumber) {
1786
+ const sessionId = sessionWorkspace.session.sessionId;
1787
+ await subscribeToPullRequest(
1788
+ {
1789
+ orgId,
1790
+ installationExternalId: project.installation.externalId,
1864
1791
  projectRepositoryId: project.id,
1865
- targets: [{ type: "pull_request", id: result.url, name: title }],
1866
- metadata: { branch: head, base, url: result.url }
1867
- }
1792
+ repositoryExternalId: project.repository.externalId,
1793
+ repositorySlug: project.repository.slug,
1794
+ changeRequestId: pullRequestNumber.toString(),
1795
+ sessionId,
1796
+ ownerId: userId,
1797
+ resourceId: sessionId,
1798
+ threadId: sessionId,
1799
+ source: "factory-pr-create",
1800
+ subscribedByUserId: userId
1801
+ },
1802
+ github.integrationStorage
1803
+ ).catch((error) => {
1804
+ console.warn(
1805
+ `[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
1806
+ error
1807
+ );
1868
1808
  });
1869
- const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
1870
- if (pullRequestNumber) {
1871
- const sessionId = sessionWorkspace.session.sessionId;
1872
- await subscribeToPullRequest(
1873
- {
1874
- orgId,
1875
- installationExternalId: project.installation.externalId,
1876
- projectRepositoryId: project.id,
1877
- repositoryExternalId: project.repository.externalId,
1878
- repositorySlug: project.repository.slug,
1879
- changeRequestId: pullRequestNumber.toString(),
1880
- sessionId,
1881
- ownerId: userId,
1882
- resourceId: sessionId,
1883
- threadId: sessionId,
1884
- source: "factory-pr-create",
1885
- subscribedByUserId: userId
1886
- },
1887
- github.integrationStorage
1888
- ).catch((error) => {
1889
- console.warn(
1890
- `[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
1891
- error
1892
- );
1893
- });
1894
- }
1895
- return c.json({ url: result.url });
1896
1809
  }
1810
+ return c.json({ url: result.url });
1897
1811
  });
1898
1812
  } catch (err) {
1899
1813
  return c.json(
@@ -1913,24 +1827,20 @@ function buildProjectGitRoutes({
1913
1827
  handler: async (c) => {
1914
1828
  const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
1915
1829
  if ("response" in owned) return owned.response;
1916
- const { userId, project, sandboxRow } = owned;
1830
+ const { sandboxRow } = owned;
1917
1831
  if (!sandboxRow.sandboxId) {
1918
1832
  return c.json({ tornDown: false });
1919
1833
  }
1920
1834
  try {
1921
- return await withProjectLock({
1922
- key: `${project.id}:${userId}`,
1923
- storage,
1924
- fn: async () => {
1925
- const sandbox = await fleet.reattachSandbox(sandboxRow.sandboxId);
1926
- await teardownProjectSandbox({
1927
- fleet,
1928
- row: sandboxRow,
1929
- storage: github.sourceControlStorage.sandboxes,
1930
- sandbox
1931
- });
1932
- return c.json({ tornDown: true });
1933
- }
1835
+ return await withSessionOperationLock(`sandbox:${sandboxRow.id}`, async () => {
1836
+ const sandbox = await fleet.reattachSandbox(sandboxRow.sandboxId);
1837
+ await teardownProjectSandbox({
1838
+ fleet,
1839
+ row: sandboxRow,
1840
+ storage: github.sourceControlStorage.sandboxes,
1841
+ sandbox
1842
+ });
1843
+ return c.json({ tornDown: true });
1934
1844
  });
1935
1845
  } catch (err) {
1936
1846
  return gitErrorResponse(loose(c), err);
@@ -1962,6 +1872,503 @@ async function resolveSessionWorkspace(github, projectId, userId, sessionId) {
1962
1872
  };
1963
1873
  }
1964
1874
 
1875
+ // src/rules/resolve.ts
1876
+ function resolveFactoryGithubRule(rules, event) {
1877
+ return rules.github[event]?.onEvent;
1878
+ }
1879
+
1880
+ // src/rules/types.ts
1881
+ var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
1882
+ var FACTORY_RULE_BOARDS = ["work", "review"];
1883
+
1884
+ // src/rules/validation.ts
1885
+ var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
1886
+ var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
1887
+ var MAX_REASON_LENGTH = 512;
1888
+ var MAX_TITLE_LENGTH = 512;
1889
+ var MAX_MESSAGE_LENGTH = 8192;
1890
+ var MAX_ARGUMENTS_LENGTH = 4096;
1891
+ var MAX_ROLE_LENGTH = 32;
1892
+ var MAX_SKILL_NAME_LENGTH = 128;
1893
+ var MAX_SOURCE_KEY_LENGTH = 256;
1894
+ var MAX_URL_LENGTH = 2048;
1895
+ var MAX_METADATA_JSON_LENGTH = 16384;
1896
+ var MAX_JSON_DEPTH = 8;
1897
+ var MAX_JSON_COLLECTION_SIZE = 100;
1898
+ var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
1899
+ var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1900
+ var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
1901
+ var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
1902
+ var REJECTION_CODES = [
1903
+ "forbidden",
1904
+ "invalid_transition",
1905
+ "missing_binding",
1906
+ "stale",
1907
+ "timeout",
1908
+ "rule_error",
1909
+ "causal_depth_exceeded",
1910
+ "repeated_transition"
1911
+ ];
1912
+ var FactoryRuleValidationError = class extends Error {
1913
+ code = "invalid_factory_rule";
1914
+ constructor(message) {
1915
+ super(message);
1916
+ this.name = "FactoryRuleValidationError";
1917
+ }
1918
+ };
1919
+ function isPlainObject(value) {
1920
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1921
+ const prototype = Object.getPrototypeOf(value);
1922
+ return prototype === Object.prototype || prototype === null;
1923
+ }
1924
+ function assertExactKeys(value, keys, label) {
1925
+ const allowed = new Set(keys);
1926
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
1927
+ throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
1928
+ }
1929
+ }
1930
+ function boundedString(value, label, max, pattern) {
1931
+ if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
1932
+ const normalized = value.trim();
1933
+ if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
1934
+ throw new FactoryRuleValidationError(`${label} is invalid.`);
1935
+ }
1936
+ return normalized;
1937
+ }
1938
+ function optionalBoundedString(value, label, max) {
1939
+ if (value === void 0) return void 0;
1940
+ return boundedString(value, label, max);
1941
+ }
1942
+ function enumValue(value, allowed, label) {
1943
+ if (typeof value !== "string" || !allowed.includes(value)) {
1944
+ throw new FactoryRuleValidationError(`${label} is invalid.`);
1945
+ }
1946
+ return value;
1947
+ }
1948
+ function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
1949
+ if (value === null || typeof value === "boolean" || typeof value === "string") return value;
1950
+ if (typeof value === "number") {
1951
+ if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
1952
+ return value;
1953
+ }
1954
+ if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
1955
+ throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
1956
+ }
1957
+ if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
1958
+ seen.add(value);
1959
+ try {
1960
+ if (Array.isArray(value)) {
1961
+ if (value.length > MAX_JSON_COLLECTION_SIZE) {
1962
+ throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
1963
+ }
1964
+ return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
1965
+ }
1966
+ if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
1967
+ const entries = Object.entries(value);
1968
+ if (entries.length > MAX_JSON_COLLECTION_SIZE) {
1969
+ throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
1970
+ }
1971
+ const sanitized = {};
1972
+ for (const [key, entry] of entries) {
1973
+ const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
1974
+ sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
1975
+ }
1976
+ return sanitized;
1977
+ } finally {
1978
+ seen.delete(value);
1979
+ }
1980
+ }
1981
+ function sanitizeMetadata(value) {
1982
+ if (value === void 0) return void 0;
1983
+ const sanitized = normalizeFactoryRuleJsonValue(value);
1984
+ if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
1985
+ if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
1986
+ throw new FactoryRuleValidationError("Rule metadata is too large.");
1987
+ }
1988
+ return sanitized;
1989
+ }
1990
+ function commonCommitFields(value) {
1991
+ return {
1992
+ idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
1993
+ };
1994
+ }
1995
+ function validateFactoryRuleDecision(value, causalDepth = 0) {
1996
+ if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
1997
+ throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
1998
+ }
1999
+ if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
2000
+ const type = value.type;
2001
+ if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
2002
+ switch (type) {
2003
+ case "reject": {
2004
+ assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
2005
+ return {
2006
+ type,
2007
+ code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
2008
+ reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
2009
+ };
2010
+ }
2011
+ case "transition": {
2012
+ assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
2013
+ return {
2014
+ type,
2015
+ ...commonCommitFields(value),
2016
+ board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
2017
+ stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
2018
+ };
2019
+ }
2020
+ case "upsertLinkedWorkItem": {
2021
+ assertExactKeys(
2022
+ value,
2023
+ ["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
2024
+ "Factory linked work item decision"
2025
+ );
2026
+ const url = value.url;
2027
+ if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
2028
+ throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
2029
+ }
2030
+ const metadata = sanitizeMetadata(value.metadata);
2031
+ return {
2032
+ type,
2033
+ ...commonCommitFields(value),
2034
+ board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
2035
+ source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
2036
+ sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
2037
+ title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
2038
+ url,
2039
+ stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
2040
+ ...metadata ? { metadata } : {}
2041
+ };
2042
+ }
2043
+ case "invokeSkill": {
2044
+ assertExactKeys(
2045
+ value,
2046
+ ["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
2047
+ "Factory invoke skill decision"
2048
+ );
2049
+ const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
2050
+ const precedingMessage = optionalBoundedString(
2051
+ value.precedingMessage,
2052
+ "Factory skill preceding message",
2053
+ MAX_MESSAGE_LENGTH
2054
+ );
2055
+ return {
2056
+ type,
2057
+ ...commonCommitFields(value),
2058
+ role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
2059
+ skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
2060
+ ...args ? { arguments: args } : {},
2061
+ ...precedingMessage ? { precedingMessage } : {}
2062
+ };
2063
+ }
2064
+ case "sendMessage": {
2065
+ assertExactKeys(
2066
+ value,
2067
+ ["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
2068
+ "Factory send message decision"
2069
+ );
2070
+ const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
2071
+ const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
2072
+ if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
2073
+ throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
2074
+ }
2075
+ return {
2076
+ type,
2077
+ ...commonCommitFields(value),
2078
+ role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
2079
+ message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
2080
+ ...priority ? { priority } : {},
2081
+ ...idleBehavior ? { idleBehavior } : {},
2082
+ ...value.prepareBinding === true ? { prepareBinding: true } : {}
2083
+ };
2084
+ }
2085
+ case "notify": {
2086
+ assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
2087
+ const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
2088
+ const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
2089
+ return {
2090
+ type,
2091
+ ...commonCommitFields(value),
2092
+ title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
2093
+ ...body ? { body } : {},
2094
+ ...level ? { level } : {}
2095
+ };
2096
+ }
2097
+ default:
2098
+ throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
2099
+ }
2100
+ }
2101
+ function validateFactoryRuleDecisions(values, causalDepth = 0) {
2102
+ if (values.length > MAX_JSON_COLLECTION_SIZE) {
2103
+ throw new FactoryRuleValidationError("Factory rule produced too many decisions.");
2104
+ }
2105
+ const decisions = [];
2106
+ for (const value of values) {
2107
+ const decision = validateFactoryRuleDecision(value, causalDepth);
2108
+ if (decision.type === "reject") {
2109
+ throw new FactoryRuleValidationError("A rejection cannot be persisted with commit decisions.");
2110
+ }
2111
+ decisions.push(decision);
2112
+ }
2113
+ const keys = decisions.map((decision) => decision.idempotencyKey);
2114
+ if (new Set(keys).size !== keys.length) {
2115
+ throw new FactoryRuleValidationError("Factory decisions require unique idempotency keys.");
2116
+ }
2117
+ return decisions;
2118
+ }
2119
+
2120
+ // src/integrations/github/rules.ts
2121
+ var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
2122
+ var RULE_TIMEOUT_MS = 5e3;
2123
+ async function withRuleTimeout(promise) {
2124
+ let timeout;
2125
+ try {
2126
+ return await Promise.race([
2127
+ promise,
2128
+ new Promise((_, reject) => {
2129
+ timeout = setTimeout(() => reject(new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
2130
+ })
2131
+ ]);
2132
+ } finally {
2133
+ if (timeout) clearTimeout(timeout);
2134
+ }
2135
+ }
2136
+ function object(value) {
2137
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2138
+ }
2139
+ function string(value) {
2140
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2141
+ }
2142
+ function number(value) {
2143
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2144
+ }
2145
+ function boolean(value) {
2146
+ return typeof value === "boolean" ? value : void 0;
2147
+ }
2148
+ function eventName(parsed) {
2149
+ const action = string(parsed.payload.action);
2150
+ if (parsed.event === "issues" && action === "opened") return "issueOpened";
2151
+ if (parsed.event === "pull_request" && action === "opened") return "pullRequestOpened";
2152
+ if (parsed.event === "pull_request" && action === "synchronize") return "pullRequestUpdated";
2153
+ if (parsed.event === "pull_request" && action === "closed" && boolean(object(parsed.payload.pull_request)?.merged)) {
2154
+ return "pullRequestMerged";
2155
+ }
2156
+ if (parsed.event === "pull_request" && action === "review_requested") return "pullRequestReviewRequested";
2157
+ return void 0;
2158
+ }
2159
+ function canonicalSourceKey(kind, itemNumber) {
2160
+ return kind === "issue" ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;
2161
+ }
2162
+ function legacySourceKey(repositoryId, kind, itemNumber) {
2163
+ return `github:${repositoryId}:${kind}:${itemNumber}`;
2164
+ }
2165
+ function provenanceTarget(repositoryId, pullRequestNumber) {
2166
+ return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;
2167
+ }
2168
+ function workItemSource(item) {
2169
+ if (!item.externalSource) return "manual";
2170
+ return item.externalSource.type === "pull-request" ? "github-pr" : "github-issue";
2171
+ }
2172
+ function workItemSourceKey(item) {
2173
+ return item.externalSource?.externalId ?? null;
2174
+ }
2175
+ async function githubActor(github, input) {
2176
+ let trusted = false;
2177
+ try {
2178
+ const permission = await github.getRepositoryCollaboratorPermission(
2179
+ input.installationId,
2180
+ input.repository,
2181
+ input.login
2182
+ );
2183
+ trusted = permission !== void 0 && TRUSTED_PERMISSIONS.has(permission);
2184
+ } catch {
2185
+ trusted = false;
2186
+ }
2187
+ return { type: "github", login: input.login, trusted, factoryAuthored: input.factoryAuthored };
2188
+ }
2189
+ function pullRequestProvenance(data) {
2190
+ if (!data || data.kind !== "factory-pr-provenance" || typeof data.workItemId !== "string") return null;
2191
+ return { kind: "factory-pr-provenance", workItemId: data.workItemId };
2192
+ }
2193
+ var GithubRules = class {
2194
+ constructor(options) {
2195
+ this.options = options;
2196
+ }
2197
+ options;
2198
+ async ingest(parsed) {
2199
+ const event = eventName(parsed);
2200
+ const repository = object(parsed.payload.repository);
2201
+ const installationId = number(object(parsed.payload.installation)?.id);
2202
+ const repositoryId = number(repository?.id);
2203
+ const repositoryName = string(repository?.full_name);
2204
+ const login = string(object(parsed.payload.sender)?.login);
2205
+ if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: "ignored" };
2206
+ const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({
2207
+ installationExternalId: String(installationId),
2208
+ repositoryExternalId: String(repositoryId)
2209
+ });
2210
+ if (projects.length === 0) return { status: "ignored" };
2211
+ const results = [];
2212
+ for (const project of projects) {
2213
+ results.push(
2214
+ await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project)
2215
+ );
2216
+ }
2217
+ if (results.some((result) => result.status === "committed")) return { status: "committed" };
2218
+ if (results.some((result) => result.status === "replayed")) return { status: "replayed" };
2219
+ return results[0] ?? { status: "ignored" };
2220
+ }
2221
+ async #ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project) {
2222
+ const factoryProject = await this.options.projects.get({
2223
+ orgId: project.orgId,
2224
+ id: project.factoryProjectId
2225
+ });
2226
+ if (!factoryProject) return { status: "missing" };
2227
+ const issue = object(parsed.payload.issue);
2228
+ const pullRequest = object(parsed.payload.pull_request);
2229
+ const issueNumber = number(issue?.number);
2230
+ const pullRequestNumber = number(pullRequest?.number);
2231
+ const provenance = pullRequestNumber ? pullRequestProvenance(
2232
+ (await this.options.integrationStorage.subscriptions.listByTarget(
2233
+ provenanceTarget(repositoryId, pullRequestNumber),
2234
+ { status: "active" }
2235
+ )).find((subscription) => subscription.orgId === project.orgId)?.data
2236
+ ) : null;
2237
+ const relatedItem = await this.#relatedItem(
2238
+ project.orgId,
2239
+ project.factoryProjectId,
2240
+ repositoryId,
2241
+ issueNumber,
2242
+ pullRequestNumber,
2243
+ string(object(pullRequest?.head)?.ref),
2244
+ provenance
2245
+ );
2246
+ const actor = await githubActor(this.options.github, {
2247
+ installationId,
2248
+ repository: repositoryName,
2249
+ login,
2250
+ factoryAuthored: provenance !== null
2251
+ });
2252
+ const context = {
2253
+ tenant: { orgId: project.orgId, projectId: project.factoryProjectId },
2254
+ actor,
2255
+ ingress: { type: "github", id: `${installationId}:${parsed.deliveryId}` },
2256
+ cause: `github.${event}`,
2257
+ causalChain: [],
2258
+ ruleSetVersion: this.options.rules.version,
2259
+ ...relatedItem ? {
2260
+ item: {
2261
+ id: relatedItem.id,
2262
+ source: workItemSource(relatedItem),
2263
+ sourceKey: workItemSourceKey(relatedItem),
2264
+ parentWorkItemId: relatedItem.parentWorkItemId,
2265
+ title: relatedItem.title,
2266
+ url: relatedItem.externalSource?.url ?? null,
2267
+ stages: relatedItem.stages
2268
+ },
2269
+ board: relatedItem.externalSource?.type === "pull-request" ? "review" : "work",
2270
+ itemRevision: relatedItem.revision
2271
+ } : {},
2272
+ event,
2273
+ deliveryId: parsed.deliveryId,
2274
+ factory: { createdAt: factoryProject.createdAt.toISOString() },
2275
+ repository: { id: repositoryId, fullName: repositoryName },
2276
+ ...issueNumber && string(issue?.title) && string(issue?.html_url) ? {
2277
+ issue: {
2278
+ number: issueNumber,
2279
+ title: string(issue?.title),
2280
+ url: string(issue?.html_url),
2281
+ ...string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}
2282
+ }
2283
+ } : {},
2284
+ ...pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url) ? {
2285
+ pullRequest: {
2286
+ number: pullRequestNumber,
2287
+ title: string(pullRequest?.title),
2288
+ url: string(pullRequest?.html_url),
2289
+ ...string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {},
2290
+ state: string(pullRequest?.state) === "closed" ? "closed" : "open",
2291
+ merged: boolean(pullRequest?.merged) ?? false,
2292
+ headBranch: string(object(pullRequest?.head)?.ref) ?? "",
2293
+ baseBranch: string(object(pullRequest?.base)?.ref) ?? ""
2294
+ }
2295
+ } : {},
2296
+ ...object(parsed.payload.review) ? {
2297
+ review: {
2298
+ id: number(object(parsed.payload.review)?.id) ?? 0,
2299
+ state: string(object(parsed.payload.review)?.state) ?? "unknown",
2300
+ url: string(object(parsed.payload.review)?.html_url) ?? ""
2301
+ }
2302
+ } : {}
2303
+ };
2304
+ const rule = resolveFactoryGithubRule(this.options.rules, event);
2305
+ let decision;
2306
+ let decisions = [];
2307
+ let outcome = { status: "accepted" };
2308
+ try {
2309
+ decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
2310
+ if (decision?.type === "reject") {
2311
+ outcome = { status: "rejected", code: decision.code, reason: decision.reason };
2312
+ } else if (decision) {
2313
+ decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
2314
+ }
2315
+ } catch (error) {
2316
+ const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
2317
+ outcome = {
2318
+ status: "rejected",
2319
+ code: timedOut ? "timeout" : "rule_error",
2320
+ reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory GitHub rule failed."
2321
+ };
2322
+ }
2323
+ const committed = await this.options.storage.commitRuleEvaluation({
2324
+ orgId: project.orgId,
2325
+ factoryProjectId: project.factoryProjectId,
2326
+ workItemId: relatedItem?.id ?? null,
2327
+ ingress: { identity: `${installationId}:${parsed.deliveryId}`, triggerType: `github.${event}` },
2328
+ ruleSetVersion: this.options.rules.version,
2329
+ expectedRevision: relatedItem?.revision ?? null,
2330
+ actor: { ...actor },
2331
+ outcome,
2332
+ decisions,
2333
+ causalChain: [],
2334
+ now: /* @__PURE__ */ new Date()
2335
+ });
2336
+ return { status: committed.status };
2337
+ }
2338
+ async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance) {
2339
+ const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });
2340
+ if (provenance) return items.find((item) => item.id === provenance.workItemId);
2341
+ if (issueNumber) {
2342
+ return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("issue", issueNumber)) ?? items.find((item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "issue", issueNumber));
2343
+ }
2344
+ if (pullRequestNumber) {
2345
+ return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber)) ?? items.find(
2346
+ (item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)
2347
+ ) ?? // Provenance fallback: a PR pushed from a work item's session branch
2348
+ // belongs to that item even when no gh-pr-create provenance was
2349
+ // recorded (session predating state seeding, or the PR was opened
2350
+ // outside the tracked tool call). Session branches are per-item
2351
+ // (`factory/issue-N`), so a head-branch match is unambiguous.
2352
+ (pullRequestHeadBranch ? items.find(
2353
+ (item) => item.externalSource?.type !== "pull-request" && Object.values(item.sessions).some((session) => session.branch === pullRequestHeadBranch)
2354
+ ) : void 0);
2355
+ }
2356
+ return void 0;
2357
+ }
2358
+ };
2359
+ function attachGithubRules(github, context) {
2360
+ if (!context.rules) return void 0;
2361
+ const rules = new GithubRules({
2362
+ github,
2363
+ sourceControl: context.storage.sourceControl,
2364
+ integrationStorage: context.storage.generic,
2365
+ projects: context.storage.projects,
2366
+ storage: context.rules.workItems,
2367
+ rules: context.rules.config
2368
+ });
2369
+ return (event) => rules.ingest(event);
2370
+ }
2371
+
1965
2372
  // src/integrations/github/session-subscriptions.ts
1966
2373
  import { createTool } from "@mastra/core/tools";
1967
2374
  import { z } from "zod";
@@ -2057,16 +2464,16 @@ async function subscriptionInput(target, pullRequestNumber) {
2057
2464
  async function subscribeCurrentSessionToPullRequest(requestContext, pullRequest, source, github) {
2058
2465
  if (source === "auto-gh-pr-create" && !isGithubProjectSession(requestContext)) return void 0;
2059
2466
  const target = await resolveSessionTarget(requestContext, github);
2060
- const number = parsePullRequest(pullRequest, target.repository.slug);
2061
- await verifyPullRequest(target, number, github);
2062
- await subscribeToPullRequest({ ...await subscriptionInput(target, number), source }, github.integrationStorage);
2063
- return number;
2467
+ const number2 = parsePullRequest(pullRequest, target.repository.slug);
2468
+ await verifyPullRequest(target, number2, github);
2469
+ await subscribeToPullRequest({ ...await subscriptionInput(target, number2), source }, github.integrationStorage);
2470
+ return number2;
2064
2471
  }
2065
2472
  async function unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github) {
2066
2473
  const target = await resolveSessionTarget(requestContext, github);
2067
- const number = parsePullRequest(pullRequest, target.repository.slug);
2068
- await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);
2069
- return number;
2474
+ const number2 = parsePullRequest(pullRequest, target.repository.slug);
2475
+ await unsubscribeFromPullRequest(await subscriptionInput(target, number2), github.integrationStorage);
2476
+ return number2;
2070
2477
  }
2071
2478
  async function refreshGithubToken(requestContext, github) {
2072
2479
  const target = await resolveSessionTarget(requestContext, github);
@@ -2106,8 +2513,8 @@ function createGithubSubscriptionTools(requestContext, github) {
2106
2513
  description: "Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.",
2107
2514
  inputSchema: pullRequestInputSchema,
2108
2515
  execute: async ({ pullRequest }) => {
2109
- const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github);
2110
- return { subscribed: true, pullRequestNumber: number };
2516
+ const number2 = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github);
2517
+ return { subscribed: true, pullRequestNumber: number2 };
2111
2518
  }
2112
2519
  }),
2113
2520
  github_unsubscribe_pr: createTool({
@@ -2115,8 +2522,8 @@ function createGithubSubscriptionTools(requestContext, github) {
2115
2522
  description: "Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.",
2116
2523
  inputSchema: pullRequestInputSchema,
2117
2524
  execute: async ({ pullRequest }) => {
2118
- const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);
2119
- return { subscribed: false, pullRequestNumber: number };
2525
+ const number2 = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);
2526
+ return { subscribed: false, pullRequestNumber: number2 };
2120
2527
  }
2121
2528
  })
2122
2529
  };
@@ -2844,6 +3251,7 @@ var PlatformGithubIntegration = class {
2844
3251
  });
2845
3252
  }
2846
3253
  routes(ctx) {
3254
+ const ingestFactoryEvent = attachGithubRules(this, ctx);
2847
3255
  return [
2848
3256
  this.#statusRoute(ctx),
2849
3257
  this.#connectRoute(ctx),
@@ -2858,7 +3266,7 @@ var PlatformGithubIntegration = class {
2858
3266
  controller: ctx.controller,
2859
3267
  projects: ctx.storage.projects,
2860
3268
  emitAudit: ctx.hooks?.emitAudit,
2861
- ingestFactoryEvent: ctx.hooks?.ingestGithubEvent
3269
+ ingestFactoryEvent
2862
3270
  }).filter(
2863
3271
  (route) => route.path !== "/web/github/status" && route.path !== "/web/github/webhook" && !route.path.startsWith("/auth/github/")
2864
3272
  )
@@ -3016,7 +3424,7 @@ var PlatformGithubIntegration = class {
3016
3424
  controller: ctx.controller,
3017
3425
  github: this,
3018
3426
  storage: ctx.storage.generic,
3019
- ingestFactoryEvent: ctx.hooks?.ingestGithubEvent,
3427
+ ingestFactoryEvent: attachGithubRules(this, ctx),
3020
3428
  intervalMs: this.#pollingIntervalMs
3021
3429
  })
3022
3430
  ];