@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
@@ -172,95 +172,8 @@ async function clearGithubPat(storage, orgId, kind = "default") {
172
172
  await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);
173
173
  }
174
174
 
175
- // src/integrations/github/project-lock.ts
176
- import { createHash } from "crypto";
177
- var inProcessLocks = /* @__PURE__ */ new Map();
178
- var DEFAULT_PROJECT_LOCK_TIMEOUT_MS = 6e4;
179
- var ProjectLockTimeoutError = class extends Error {
180
- key;
181
- timeoutMs;
182
- constructor(key, timeoutMs) {
183
- super(`Project lock critical section for "${key}" exceeded ${timeoutMs}ms`);
184
- this.name = "ProjectLockTimeoutError";
185
- this.key = key;
186
- this.timeoutMs = timeoutMs;
187
- }
188
- };
189
- async function runWithTimeout(key, timeoutMs, fn) {
190
- const controller = new AbortController();
191
- const timer = setTimeout(() => controller.abort(), timeoutMs);
192
- const abortError = new Promise((_, reject) => {
193
- controller.signal.addEventListener("abort", () => reject(new ProjectLockTimeoutError(key, timeoutMs)), {
194
- once: true
195
- });
196
- });
197
- const work = fn(controller.signal);
198
- work.catch(() => {
199
- });
200
- try {
201
- return await Promise.race([work, abortError]);
202
- } finally {
203
- clearTimeout(timer);
204
- }
205
- }
206
- function hashKey(key) {
207
- const digest = createHash("sha256").update(key).digest();
208
- const a = digest.readInt32BE(0);
209
- const b = digest.readInt32BE(4);
210
- return [a, b];
211
- }
212
- function withProjectLock(options) {
213
- const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
214
- const prev = inProcessLocks.get(key) ?? Promise.resolve();
215
- const run = () => withDbAdvisoryLock({ key, storage, fn, pool, timeoutMs });
216
- const next = prev.then(run, run);
217
- const tail = next.then(
218
- () => void 0,
219
- () => void 0
220
- );
221
- inProcessLocks.set(key, tail);
222
- void tail.then(() => {
223
- if (inProcessLocks.get(key) === tail) {
224
- inProcessLocks.delete(key);
225
- }
226
- });
227
- return next;
228
- }
229
- async function withDbAdvisoryLock(options) {
230
- const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
231
- if (process.env.MASTRACODE_DISTRIBUTED_LOCK === "0") {
232
- return runWithTimeout(key, timeoutMs, fn);
233
- }
234
- if (pool) return advisoryLockOver(pool, key, timeoutMs, fn);
235
- if (typeof storage?.withDistributedLock !== "function") {
236
- return runWithTimeout(key, timeoutMs, fn);
237
- }
238
- return storage.withDistributedLock(key, () => runWithTimeout(key, timeoutMs, fn));
239
- }
240
- async function advisoryLockOver(pool, key, timeoutMs, fn) {
241
- const [k1, k2] = hashKey(key);
242
- const client = await pool.connect();
243
- try {
244
- await client.query("BEGIN");
245
- await client.query("SELECT pg_advisory_xact_lock($1, $2)", [k1, k2]);
246
- try {
247
- const result = await runWithTimeout(key, timeoutMs, fn);
248
- await client.query("COMMIT");
249
- return result;
250
- } catch (err) {
251
- try {
252
- await client.query("ROLLBACK");
253
- } catch {
254
- }
255
- throw err;
256
- }
257
- } finally {
258
- client.release();
259
- }
260
- }
261
-
262
175
  // src/integrations/github/sandbox.ts
263
- import { createHash as createHash2 } from "crypto";
176
+ import { createHash } from "crypto";
264
177
  function bindingStore(row, storage) {
265
178
  return {
266
179
  sandboxId: row.sandboxId,
@@ -456,7 +369,7 @@ var WorktreeError = class extends Error {
456
369
  function safeBranchDir(branch) {
457
370
  const sanitized = branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\/+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100) || "work";
458
371
  if (sanitized === branch) return sanitized;
459
- const hash = createHash2("sha256").update(branch).digest("hex").slice(0, 8);
372
+ const hash = createHash("sha256").update(branch).digest("hex").slice(0, 8);
460
373
  return `${sanitized}-${hash}`;
461
374
  }
462
375
  function computeWorktreePath(repoWorkdir, branch) {
@@ -878,6 +791,20 @@ async function handleGithubWebhook(c, options) {
878
791
  }
879
792
 
880
793
  // src/integrations/github/routes.ts
794
+ var sessionOperationLocks = /* @__PURE__ */ new Map();
795
+ function withSessionOperationLock(sessionId, fn) {
796
+ const previous = sessionOperationLocks.get(sessionId) ?? Promise.resolve();
797
+ const next = previous.then(fn, fn);
798
+ const tail = next.then(
799
+ () => void 0,
800
+ () => void 0
801
+ );
802
+ sessionOperationLocks.set(sessionId, tail);
803
+ void tail.then(() => {
804
+ if (sessionOperationLocks.get(sessionId) === tail) sessionOperationLocks.delete(sessionId);
805
+ });
806
+ return next;
807
+ }
881
808
  function loose(c) {
882
809
  return c;
883
810
  }
@@ -897,8 +824,8 @@ function pullRequestNumberFromUrl(value, expectedRepo) {
897
824
  if (url.protocol !== "https:" || url.hostname !== "github.com" || match?.[1]?.toLowerCase() !== expectedRepo.toLowerCase()) {
898
825
  return void 0;
899
826
  }
900
- const number = Number(match[2]);
901
- return Number.isInteger(number) && number > 0 ? number : void 0;
827
+ const number2 = Number(match[2]);
828
+ return Number.isInteger(number2) && number2 > 0 ? number2 : void 0;
902
829
  } catch {
903
830
  return void 0;
904
831
  }
@@ -1562,7 +1489,7 @@ function buildGithubRoutes(options) {
1562
1489
  }
1563
1490
  })
1564
1491
  );
1565
- routes.push(...buildProjectGitRoutes({ github, auth, fleet, storage, emitAudit }));
1492
+ routes.push(...buildProjectGitRoutes({ github, auth, fleet, emitAudit }));
1566
1493
  return routes;
1567
1494
  }
1568
1495
  async function loadOrgProject(options) {
@@ -1679,7 +1606,6 @@ function buildProjectGitRoutes({
1679
1606
  github,
1680
1607
  auth,
1681
1608
  fleet,
1682
- storage,
1683
1609
  emitAudit
1684
1610
  }) {
1685
1611
  return [
@@ -1800,31 +1726,27 @@ function buildProjectGitRoutes({
1800
1726
  }
1801
1727
  const { workdir, sandboxBinding } = sessionWorkspace;
1802
1728
  try {
1803
- return await withProjectLock({
1804
- key: `${project.id}:${userId}`,
1805
- storage,
1806
- fn: async () => {
1807
- const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1808
- const result = await commitAll(
1809
- sandbox,
1810
- workdir,
1811
- body.message,
1812
- identityFromUser(await auth.ensureUser(loose(c)))
1813
- );
1814
- if (result.committed) {
1815
- await emitAudit?.({
1816
- context: loose(c),
1817
- input: {
1818
- action: "factory.git.commit",
1819
- factoryProjectId: project.factoryProjectId,
1820
- projectRepositoryId: project.id,
1821
- targets: [{ type: "session", id: sessionWorkspace.session.sessionId }],
1822
- metadata: { sessionId: sessionWorkspace.session.sessionId }
1823
- }
1824
- });
1825
- }
1826
- return c.json({ committed: result.committed });
1729
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1730
+ const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1731
+ const result = await commitAll(
1732
+ sandbox,
1733
+ workdir,
1734
+ body.message,
1735
+ identityFromUser(await auth.ensureUser(loose(c)))
1736
+ );
1737
+ if (result.committed) {
1738
+ await emitAudit?.({
1739
+ context: loose(c),
1740
+ input: {
1741
+ action: "factory.git.commit",
1742
+ factoryProjectId: project.factoryProjectId,
1743
+ projectRepositoryId: project.id,
1744
+ targets: [{ type: "session", id: sessionWorkspace.session.sessionId }],
1745
+ metadata: { sessionId: sessionWorkspace.session.sessionId }
1746
+ }
1747
+ });
1827
1748
  }
1749
+ return c.json({ committed: result.committed });
1828
1750
  });
1829
1751
  } catch (err) {
1830
1752
  return gitErrorResponse(loose(c), err);
@@ -1855,29 +1777,25 @@ function buildProjectGitRoutes({
1855
1777
  }
1856
1778
  const { workdir, sandboxBinding } = sessionWorkspace;
1857
1779
  try {
1858
- return await withProjectLock({
1859
- key: `${project.id}:${userId}`,
1860
- storage,
1861
- fn: async () => {
1862
- const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1863
- const access = await github.versionControl.getRepositoryAccess({
1864
- orgId,
1865
- repositoryId: project.repository.id
1866
- });
1867
- if (!access.authorization) throw new Error("Repository access did not include a bearer token.");
1868
- await pushBranch(sandbox, workdir, branch, access.authorization.token, project.repository.slug);
1869
- await emitAudit?.({
1870
- context: loose(c),
1871
- input: {
1872
- action: "factory.git.push",
1873
- factoryProjectId: project.factoryProjectId,
1874
- projectRepositoryId: project.id,
1875
- targets: [{ type: "branch", id: branch }],
1876
- metadata: { branch, sessionId: sessionWorkspace.session.sessionId }
1877
- }
1878
- });
1879
- return c.json({ pushed: true, branch });
1880
- }
1780
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1781
+ const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
1782
+ const access = await github.versionControl.getRepositoryAccess({
1783
+ orgId,
1784
+ repositoryId: project.repository.id
1785
+ });
1786
+ if (!access.authorization) throw new Error("Repository access did not include a bearer token.");
1787
+ await pushBranch(sandbox, workdir, branch, access.authorization.token, project.repository.slug);
1788
+ await emitAudit?.({
1789
+ context: loose(c),
1790
+ input: {
1791
+ action: "factory.git.push",
1792
+ factoryProjectId: project.factoryProjectId,
1793
+ projectRepositoryId: project.id,
1794
+ targets: [{ type: "branch", id: branch }],
1795
+ metadata: { branch, sessionId: sessionWorkspace.session.sessionId }
1796
+ }
1797
+ });
1798
+ return c.json({ pushed: true, branch });
1881
1799
  });
1882
1800
  } catch (err) {
1883
1801
  return gitErrorResponse(loose(c), err);
@@ -1919,60 +1837,56 @@ function buildProjectGitRoutes({
1919
1837
  return c.json({ error: "Invalid sessionId" }, 400);
1920
1838
  }
1921
1839
  try {
1922
- return await withProjectLock({
1923
- key: `${project.id}:${userId}`,
1924
- storage,
1925
- fn: async () => {
1926
- const result = await github.versionControl.createPullRequest({
1927
- connection: {
1928
- type: "app-installation",
1929
- installationId: Number(project.installation.externalId)
1930
- },
1931
- sourceId: project.repository.slug,
1932
- baseBranch: base,
1933
- headBranch: head,
1934
- title,
1935
- body: prBody,
1936
- actingUserId: userId
1937
- });
1938
- await emitAudit?.({
1939
- context: loose(c),
1940
- input: {
1941
- action: "factory.git.pr_opened",
1942
- factoryProjectId: project.factoryProjectId,
1840
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1841
+ const result = await github.versionControl.createPullRequest({
1842
+ connection: {
1843
+ type: "app-installation",
1844
+ installationId: Number(project.installation.externalId)
1845
+ },
1846
+ sourceId: project.repository.slug,
1847
+ baseBranch: base,
1848
+ headBranch: head,
1849
+ title,
1850
+ body: prBody,
1851
+ actingUserId: userId
1852
+ });
1853
+ await emitAudit?.({
1854
+ context: loose(c),
1855
+ input: {
1856
+ action: "factory.git.pr_opened",
1857
+ factoryProjectId: project.factoryProjectId,
1858
+ projectRepositoryId: project.id,
1859
+ targets: [{ type: "pull_request", id: result.url, name: title }],
1860
+ metadata: { branch: head, base, url: result.url }
1861
+ }
1862
+ });
1863
+ const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
1864
+ if (pullRequestNumber) {
1865
+ const sessionId = sessionWorkspace.session.sessionId;
1866
+ await subscribeToPullRequest(
1867
+ {
1868
+ orgId,
1869
+ installationExternalId: project.installation.externalId,
1943
1870
  projectRepositoryId: project.id,
1944
- targets: [{ type: "pull_request", id: result.url, name: title }],
1945
- metadata: { branch: head, base, url: result.url }
1946
- }
1871
+ repositoryExternalId: project.repository.externalId,
1872
+ repositorySlug: project.repository.slug,
1873
+ changeRequestId: pullRequestNumber.toString(),
1874
+ sessionId,
1875
+ ownerId: userId,
1876
+ resourceId: sessionId,
1877
+ threadId: sessionId,
1878
+ source: "factory-pr-create",
1879
+ subscribedByUserId: userId
1880
+ },
1881
+ github.integrationStorage
1882
+ ).catch((error) => {
1883
+ console.warn(
1884
+ `[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
1885
+ error
1886
+ );
1947
1887
  });
1948
- const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
1949
- if (pullRequestNumber) {
1950
- const sessionId = sessionWorkspace.session.sessionId;
1951
- await subscribeToPullRequest(
1952
- {
1953
- orgId,
1954
- installationExternalId: project.installation.externalId,
1955
- projectRepositoryId: project.id,
1956
- repositoryExternalId: project.repository.externalId,
1957
- repositorySlug: project.repository.slug,
1958
- changeRequestId: pullRequestNumber.toString(),
1959
- sessionId,
1960
- ownerId: userId,
1961
- resourceId: sessionId,
1962
- threadId: sessionId,
1963
- source: "factory-pr-create",
1964
- subscribedByUserId: userId
1965
- },
1966
- github.integrationStorage
1967
- ).catch((error) => {
1968
- console.warn(
1969
- `[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
1970
- error
1971
- );
1972
- });
1973
- }
1974
- return c.json({ url: result.url });
1975
1888
  }
1889
+ return c.json({ url: result.url });
1976
1890
  });
1977
1891
  } catch (err) {
1978
1892
  return c.json(
@@ -1992,24 +1906,20 @@ function buildProjectGitRoutes({
1992
1906
  handler: async (c) => {
1993
1907
  const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
1994
1908
  if ("response" in owned) return owned.response;
1995
- const { userId, project, sandboxRow } = owned;
1909
+ const { sandboxRow } = owned;
1996
1910
  if (!sandboxRow.sandboxId) {
1997
1911
  return c.json({ tornDown: false });
1998
1912
  }
1999
1913
  try {
2000
- return await withProjectLock({
2001
- key: `${project.id}:${userId}`,
2002
- storage,
2003
- fn: async () => {
2004
- const sandbox = await fleet.reattachSandbox(sandboxRow.sandboxId);
2005
- await teardownProjectSandbox({
2006
- fleet,
2007
- row: sandboxRow,
2008
- storage: github.sourceControlStorage.sandboxes,
2009
- sandbox
2010
- });
2011
- return c.json({ tornDown: true });
2012
- }
1914
+ return await withSessionOperationLock(`sandbox:${sandboxRow.id}`, async () => {
1915
+ const sandbox = await fleet.reattachSandbox(sandboxRow.sandboxId);
1916
+ await teardownProjectSandbox({
1917
+ fleet,
1918
+ row: sandboxRow,
1919
+ storage: github.sourceControlStorage.sandboxes,
1920
+ sandbox
1921
+ });
1922
+ return c.json({ tornDown: true });
2013
1923
  });
2014
1924
  } catch (err) {
2015
1925
  return gitErrorResponse(loose(c), err);
@@ -2041,6 +1951,503 @@ async function resolveSessionWorkspace(github, projectId, userId, sessionId) {
2041
1951
  };
2042
1952
  }
2043
1953
 
1954
+ // src/rules/resolve.ts
1955
+ function resolveFactoryGithubRule(rules, event) {
1956
+ return rules.github[event]?.onEvent;
1957
+ }
1958
+
1959
+ // src/rules/types.ts
1960
+ var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
1961
+ var FACTORY_RULE_BOARDS = ["work", "review"];
1962
+
1963
+ // src/rules/validation.ts
1964
+ var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
1965
+ var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
1966
+ var MAX_REASON_LENGTH = 512;
1967
+ var MAX_TITLE_LENGTH = 512;
1968
+ var MAX_MESSAGE_LENGTH = 8192;
1969
+ var MAX_ARGUMENTS_LENGTH = 4096;
1970
+ var MAX_ROLE_LENGTH = 32;
1971
+ var MAX_SKILL_NAME_LENGTH = 128;
1972
+ var MAX_SOURCE_KEY_LENGTH = 256;
1973
+ var MAX_URL_LENGTH = 2048;
1974
+ var MAX_METADATA_JSON_LENGTH = 16384;
1975
+ var MAX_JSON_DEPTH = 8;
1976
+ var MAX_JSON_COLLECTION_SIZE = 100;
1977
+ var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
1978
+ var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1979
+ var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
1980
+ var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
1981
+ var REJECTION_CODES = [
1982
+ "forbidden",
1983
+ "invalid_transition",
1984
+ "missing_binding",
1985
+ "stale",
1986
+ "timeout",
1987
+ "rule_error",
1988
+ "causal_depth_exceeded",
1989
+ "repeated_transition"
1990
+ ];
1991
+ var FactoryRuleValidationError = class extends Error {
1992
+ code = "invalid_factory_rule";
1993
+ constructor(message) {
1994
+ super(message);
1995
+ this.name = "FactoryRuleValidationError";
1996
+ }
1997
+ };
1998
+ function isPlainObject(value) {
1999
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2000
+ const prototype = Object.getPrototypeOf(value);
2001
+ return prototype === Object.prototype || prototype === null;
2002
+ }
2003
+ function assertExactKeys(value, keys, label) {
2004
+ const allowed = new Set(keys);
2005
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
2006
+ throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
2007
+ }
2008
+ }
2009
+ function boundedString(value, label, max, pattern) {
2010
+ if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
2011
+ const normalized = value.trim();
2012
+ if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
2013
+ throw new FactoryRuleValidationError(`${label} is invalid.`);
2014
+ }
2015
+ return normalized;
2016
+ }
2017
+ function optionalBoundedString(value, label, max) {
2018
+ if (value === void 0) return void 0;
2019
+ return boundedString(value, label, max);
2020
+ }
2021
+ function enumValue(value, allowed, label) {
2022
+ if (typeof value !== "string" || !allowed.includes(value)) {
2023
+ throw new FactoryRuleValidationError(`${label} is invalid.`);
2024
+ }
2025
+ return value;
2026
+ }
2027
+ function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
2028
+ if (value === null || typeof value === "boolean" || typeof value === "string") return value;
2029
+ if (typeof value === "number") {
2030
+ if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
2031
+ return value;
2032
+ }
2033
+ if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
2034
+ throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
2035
+ }
2036
+ if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
2037
+ seen.add(value);
2038
+ try {
2039
+ if (Array.isArray(value)) {
2040
+ if (value.length > MAX_JSON_COLLECTION_SIZE) {
2041
+ throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
2042
+ }
2043
+ return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
2044
+ }
2045
+ if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
2046
+ const entries = Object.entries(value);
2047
+ if (entries.length > MAX_JSON_COLLECTION_SIZE) {
2048
+ throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
2049
+ }
2050
+ const sanitized = {};
2051
+ for (const [key, entry] of entries) {
2052
+ const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
2053
+ sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
2054
+ }
2055
+ return sanitized;
2056
+ } finally {
2057
+ seen.delete(value);
2058
+ }
2059
+ }
2060
+ function sanitizeMetadata(value) {
2061
+ if (value === void 0) return void 0;
2062
+ const sanitized = normalizeFactoryRuleJsonValue(value);
2063
+ if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
2064
+ if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
2065
+ throw new FactoryRuleValidationError("Rule metadata is too large.");
2066
+ }
2067
+ return sanitized;
2068
+ }
2069
+ function commonCommitFields(value) {
2070
+ return {
2071
+ idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
2072
+ };
2073
+ }
2074
+ function validateFactoryRuleDecision(value, causalDepth = 0) {
2075
+ if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
2076
+ throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
2077
+ }
2078
+ if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
2079
+ const type = value.type;
2080
+ if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
2081
+ switch (type) {
2082
+ case "reject": {
2083
+ assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
2084
+ return {
2085
+ type,
2086
+ code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
2087
+ reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
2088
+ };
2089
+ }
2090
+ case "transition": {
2091
+ assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
2092
+ return {
2093
+ type,
2094
+ ...commonCommitFields(value),
2095
+ board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
2096
+ stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
2097
+ };
2098
+ }
2099
+ case "upsertLinkedWorkItem": {
2100
+ assertExactKeys(
2101
+ value,
2102
+ ["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
2103
+ "Factory linked work item decision"
2104
+ );
2105
+ const url = value.url;
2106
+ if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
2107
+ throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
2108
+ }
2109
+ const metadata = sanitizeMetadata(value.metadata);
2110
+ return {
2111
+ type,
2112
+ ...commonCommitFields(value),
2113
+ board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
2114
+ source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
2115
+ sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
2116
+ title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
2117
+ url,
2118
+ stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
2119
+ ...metadata ? { metadata } : {}
2120
+ };
2121
+ }
2122
+ case "invokeSkill": {
2123
+ assertExactKeys(
2124
+ value,
2125
+ ["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
2126
+ "Factory invoke skill decision"
2127
+ );
2128
+ const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
2129
+ const precedingMessage = optionalBoundedString(
2130
+ value.precedingMessage,
2131
+ "Factory skill preceding message",
2132
+ MAX_MESSAGE_LENGTH
2133
+ );
2134
+ return {
2135
+ type,
2136
+ ...commonCommitFields(value),
2137
+ role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
2138
+ skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
2139
+ ...args ? { arguments: args } : {},
2140
+ ...precedingMessage ? { precedingMessage } : {}
2141
+ };
2142
+ }
2143
+ case "sendMessage": {
2144
+ assertExactKeys(
2145
+ value,
2146
+ ["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
2147
+ "Factory send message decision"
2148
+ );
2149
+ const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
2150
+ const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
2151
+ if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
2152
+ throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
2153
+ }
2154
+ return {
2155
+ type,
2156
+ ...commonCommitFields(value),
2157
+ role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
2158
+ message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
2159
+ ...priority ? { priority } : {},
2160
+ ...idleBehavior ? { idleBehavior } : {},
2161
+ ...value.prepareBinding === true ? { prepareBinding: true } : {}
2162
+ };
2163
+ }
2164
+ case "notify": {
2165
+ assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
2166
+ const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
2167
+ const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
2168
+ return {
2169
+ type,
2170
+ ...commonCommitFields(value),
2171
+ title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
2172
+ ...body ? { body } : {},
2173
+ ...level ? { level } : {}
2174
+ };
2175
+ }
2176
+ default:
2177
+ throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
2178
+ }
2179
+ }
2180
+ function validateFactoryRuleDecisions(values, causalDepth = 0) {
2181
+ if (values.length > MAX_JSON_COLLECTION_SIZE) {
2182
+ throw new FactoryRuleValidationError("Factory rule produced too many decisions.");
2183
+ }
2184
+ const decisions = [];
2185
+ for (const value of values) {
2186
+ const decision = validateFactoryRuleDecision(value, causalDepth);
2187
+ if (decision.type === "reject") {
2188
+ throw new FactoryRuleValidationError("A rejection cannot be persisted with commit decisions.");
2189
+ }
2190
+ decisions.push(decision);
2191
+ }
2192
+ const keys = decisions.map((decision) => decision.idempotencyKey);
2193
+ if (new Set(keys).size !== keys.length) {
2194
+ throw new FactoryRuleValidationError("Factory decisions require unique idempotency keys.");
2195
+ }
2196
+ return decisions;
2197
+ }
2198
+
2199
+ // src/integrations/github/rules.ts
2200
+ var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
2201
+ var RULE_TIMEOUT_MS = 5e3;
2202
+ async function withRuleTimeout(promise) {
2203
+ let timeout;
2204
+ try {
2205
+ return await Promise.race([
2206
+ promise,
2207
+ new Promise((_, reject) => {
2208
+ timeout = setTimeout(() => reject(new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
2209
+ })
2210
+ ]);
2211
+ } finally {
2212
+ if (timeout) clearTimeout(timeout);
2213
+ }
2214
+ }
2215
+ function object(value) {
2216
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2217
+ }
2218
+ function string(value) {
2219
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2220
+ }
2221
+ function number(value) {
2222
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2223
+ }
2224
+ function boolean(value) {
2225
+ return typeof value === "boolean" ? value : void 0;
2226
+ }
2227
+ function eventName(parsed) {
2228
+ const action = string(parsed.payload.action);
2229
+ if (parsed.event === "issues" && action === "opened") return "issueOpened";
2230
+ if (parsed.event === "pull_request" && action === "opened") return "pullRequestOpened";
2231
+ if (parsed.event === "pull_request" && action === "synchronize") return "pullRequestUpdated";
2232
+ if (parsed.event === "pull_request" && action === "closed" && boolean(object(parsed.payload.pull_request)?.merged)) {
2233
+ return "pullRequestMerged";
2234
+ }
2235
+ if (parsed.event === "pull_request" && action === "review_requested") return "pullRequestReviewRequested";
2236
+ return void 0;
2237
+ }
2238
+ function canonicalSourceKey(kind, itemNumber) {
2239
+ return kind === "issue" ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;
2240
+ }
2241
+ function legacySourceKey(repositoryId, kind, itemNumber) {
2242
+ return `github:${repositoryId}:${kind}:${itemNumber}`;
2243
+ }
2244
+ function provenanceTarget(repositoryId, pullRequestNumber) {
2245
+ return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;
2246
+ }
2247
+ function workItemSource(item) {
2248
+ if (!item.externalSource) return "manual";
2249
+ return item.externalSource.type === "pull-request" ? "github-pr" : "github-issue";
2250
+ }
2251
+ function workItemSourceKey(item) {
2252
+ return item.externalSource?.externalId ?? null;
2253
+ }
2254
+ async function githubActor(github, input) {
2255
+ let trusted = false;
2256
+ try {
2257
+ const permission = await github.getRepositoryCollaboratorPermission(
2258
+ input.installationId,
2259
+ input.repository,
2260
+ input.login
2261
+ );
2262
+ trusted = permission !== void 0 && TRUSTED_PERMISSIONS.has(permission);
2263
+ } catch {
2264
+ trusted = false;
2265
+ }
2266
+ return { type: "github", login: input.login, trusted, factoryAuthored: input.factoryAuthored };
2267
+ }
2268
+ function pullRequestProvenance(data) {
2269
+ if (!data || data.kind !== "factory-pr-provenance" || typeof data.workItemId !== "string") return null;
2270
+ return { kind: "factory-pr-provenance", workItemId: data.workItemId };
2271
+ }
2272
+ var GithubRules = class {
2273
+ constructor(options) {
2274
+ this.options = options;
2275
+ }
2276
+ options;
2277
+ async ingest(parsed) {
2278
+ const event = eventName(parsed);
2279
+ const repository = object(parsed.payload.repository);
2280
+ const installationId = number(object(parsed.payload.installation)?.id);
2281
+ const repositoryId = number(repository?.id);
2282
+ const repositoryName = string(repository?.full_name);
2283
+ const login = string(object(parsed.payload.sender)?.login);
2284
+ if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: "ignored" };
2285
+ const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({
2286
+ installationExternalId: String(installationId),
2287
+ repositoryExternalId: String(repositoryId)
2288
+ });
2289
+ if (projects.length === 0) return { status: "ignored" };
2290
+ const results = [];
2291
+ for (const project of projects) {
2292
+ results.push(
2293
+ await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project)
2294
+ );
2295
+ }
2296
+ if (results.some((result) => result.status === "committed")) return { status: "committed" };
2297
+ if (results.some((result) => result.status === "replayed")) return { status: "replayed" };
2298
+ return results[0] ?? { status: "ignored" };
2299
+ }
2300
+ async #ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project) {
2301
+ const factoryProject = await this.options.projects.get({
2302
+ orgId: project.orgId,
2303
+ id: project.factoryProjectId
2304
+ });
2305
+ if (!factoryProject) return { status: "missing" };
2306
+ const issue = object(parsed.payload.issue);
2307
+ const pullRequest = object(parsed.payload.pull_request);
2308
+ const issueNumber = number(issue?.number);
2309
+ const pullRequestNumber = number(pullRequest?.number);
2310
+ const provenance = pullRequestNumber ? pullRequestProvenance(
2311
+ (await this.options.integrationStorage.subscriptions.listByTarget(
2312
+ provenanceTarget(repositoryId, pullRequestNumber),
2313
+ { status: "active" }
2314
+ )).find((subscription) => subscription.orgId === project.orgId)?.data
2315
+ ) : null;
2316
+ const relatedItem = await this.#relatedItem(
2317
+ project.orgId,
2318
+ project.factoryProjectId,
2319
+ repositoryId,
2320
+ issueNumber,
2321
+ pullRequestNumber,
2322
+ string(object(pullRequest?.head)?.ref),
2323
+ provenance
2324
+ );
2325
+ const actor = await githubActor(this.options.github, {
2326
+ installationId,
2327
+ repository: repositoryName,
2328
+ login,
2329
+ factoryAuthored: provenance !== null
2330
+ });
2331
+ const context = {
2332
+ tenant: { orgId: project.orgId, projectId: project.factoryProjectId },
2333
+ actor,
2334
+ ingress: { type: "github", id: `${installationId}:${parsed.deliveryId}` },
2335
+ cause: `github.${event}`,
2336
+ causalChain: [],
2337
+ ruleSetVersion: this.options.rules.version,
2338
+ ...relatedItem ? {
2339
+ item: {
2340
+ id: relatedItem.id,
2341
+ source: workItemSource(relatedItem),
2342
+ sourceKey: workItemSourceKey(relatedItem),
2343
+ parentWorkItemId: relatedItem.parentWorkItemId,
2344
+ title: relatedItem.title,
2345
+ url: relatedItem.externalSource?.url ?? null,
2346
+ stages: relatedItem.stages
2347
+ },
2348
+ board: relatedItem.externalSource?.type === "pull-request" ? "review" : "work",
2349
+ itemRevision: relatedItem.revision
2350
+ } : {},
2351
+ event,
2352
+ deliveryId: parsed.deliveryId,
2353
+ factory: { createdAt: factoryProject.createdAt.toISOString() },
2354
+ repository: { id: repositoryId, fullName: repositoryName },
2355
+ ...issueNumber && string(issue?.title) && string(issue?.html_url) ? {
2356
+ issue: {
2357
+ number: issueNumber,
2358
+ title: string(issue?.title),
2359
+ url: string(issue?.html_url),
2360
+ ...string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}
2361
+ }
2362
+ } : {},
2363
+ ...pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url) ? {
2364
+ pullRequest: {
2365
+ number: pullRequestNumber,
2366
+ title: string(pullRequest?.title),
2367
+ url: string(pullRequest?.html_url),
2368
+ ...string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {},
2369
+ state: string(pullRequest?.state) === "closed" ? "closed" : "open",
2370
+ merged: boolean(pullRequest?.merged) ?? false,
2371
+ headBranch: string(object(pullRequest?.head)?.ref) ?? "",
2372
+ baseBranch: string(object(pullRequest?.base)?.ref) ?? ""
2373
+ }
2374
+ } : {},
2375
+ ...object(parsed.payload.review) ? {
2376
+ review: {
2377
+ id: number(object(parsed.payload.review)?.id) ?? 0,
2378
+ state: string(object(parsed.payload.review)?.state) ?? "unknown",
2379
+ url: string(object(parsed.payload.review)?.html_url) ?? ""
2380
+ }
2381
+ } : {}
2382
+ };
2383
+ const rule = resolveFactoryGithubRule(this.options.rules, event);
2384
+ let decision;
2385
+ let decisions = [];
2386
+ let outcome = { status: "accepted" };
2387
+ try {
2388
+ decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
2389
+ if (decision?.type === "reject") {
2390
+ outcome = { status: "rejected", code: decision.code, reason: decision.reason };
2391
+ } else if (decision) {
2392
+ decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
2393
+ }
2394
+ } catch (error) {
2395
+ const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
2396
+ outcome = {
2397
+ status: "rejected",
2398
+ code: timedOut ? "timeout" : "rule_error",
2399
+ reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory GitHub rule failed."
2400
+ };
2401
+ }
2402
+ const committed = await this.options.storage.commitRuleEvaluation({
2403
+ orgId: project.orgId,
2404
+ factoryProjectId: project.factoryProjectId,
2405
+ workItemId: relatedItem?.id ?? null,
2406
+ ingress: { identity: `${installationId}:${parsed.deliveryId}`, triggerType: `github.${event}` },
2407
+ ruleSetVersion: this.options.rules.version,
2408
+ expectedRevision: relatedItem?.revision ?? null,
2409
+ actor: { ...actor },
2410
+ outcome,
2411
+ decisions,
2412
+ causalChain: [],
2413
+ now: /* @__PURE__ */ new Date()
2414
+ });
2415
+ return { status: committed.status };
2416
+ }
2417
+ async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance) {
2418
+ const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });
2419
+ if (provenance) return items.find((item) => item.id === provenance.workItemId);
2420
+ if (issueNumber) {
2421
+ return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("issue", issueNumber)) ?? items.find((item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "issue", issueNumber));
2422
+ }
2423
+ if (pullRequestNumber) {
2424
+ return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber)) ?? items.find(
2425
+ (item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)
2426
+ ) ?? // Provenance fallback: a PR pushed from a work item's session branch
2427
+ // belongs to that item even when no gh-pr-create provenance was
2428
+ // recorded (session predating state seeding, or the PR was opened
2429
+ // outside the tracked tool call). Session branches are per-item
2430
+ // (`factory/issue-N`), so a head-branch match is unambiguous.
2431
+ (pullRequestHeadBranch ? items.find(
2432
+ (item) => item.externalSource?.type !== "pull-request" && Object.values(item.sessions).some((session) => session.branch === pullRequestHeadBranch)
2433
+ ) : void 0);
2434
+ }
2435
+ return void 0;
2436
+ }
2437
+ };
2438
+ function attachGithubRules(github, context) {
2439
+ if (!context.rules) return void 0;
2440
+ const rules = new GithubRules({
2441
+ github,
2442
+ sourceControl: context.storage.sourceControl,
2443
+ integrationStorage: context.storage.generic,
2444
+ projects: context.storage.projects,
2445
+ storage: context.rules.workItems,
2446
+ rules: context.rules.config
2447
+ });
2448
+ return (event) => rules.ingest(event);
2449
+ }
2450
+
2044
2451
  // src/integrations/github/session-subscriptions.ts
2045
2452
  import { createTool } from "@mastra/core/tools";
2046
2453
  import { z } from "zod";
@@ -2136,16 +2543,16 @@ async function subscriptionInput(target, pullRequestNumber) {
2136
2543
  async function subscribeCurrentSessionToPullRequest(requestContext, pullRequest, source, github) {
2137
2544
  if (source === "auto-gh-pr-create" && !isGithubProjectSession(requestContext)) return void 0;
2138
2545
  const target = await resolveSessionTarget(requestContext, github);
2139
- const number = parsePullRequest(pullRequest, target.repository.slug);
2140
- await verifyPullRequest(target, number, github);
2141
- await subscribeToPullRequest({ ...await subscriptionInput(target, number), source }, github.integrationStorage);
2142
- return number;
2546
+ const number2 = parsePullRequest(pullRequest, target.repository.slug);
2547
+ await verifyPullRequest(target, number2, github);
2548
+ await subscribeToPullRequest({ ...await subscriptionInput(target, number2), source }, github.integrationStorage);
2549
+ return number2;
2143
2550
  }
2144
2551
  async function unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github) {
2145
2552
  const target = await resolveSessionTarget(requestContext, github);
2146
- const number = parsePullRequest(pullRequest, target.repository.slug);
2147
- await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);
2148
- return number;
2553
+ const number2 = parsePullRequest(pullRequest, target.repository.slug);
2554
+ await unsubscribeFromPullRequest(await subscriptionInput(target, number2), github.integrationStorage);
2555
+ return number2;
2149
2556
  }
2150
2557
  async function refreshGithubToken(requestContext, github) {
2151
2558
  const target = await resolveSessionTarget(requestContext, github);
@@ -2185,8 +2592,8 @@ function createGithubSubscriptionTools(requestContext, github) {
2185
2592
  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.",
2186
2593
  inputSchema: pullRequestInputSchema,
2187
2594
  execute: async ({ pullRequest }) => {
2188
- const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github);
2189
- return { subscribed: true, pullRequestNumber: number };
2595
+ const number2 = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github);
2596
+ return { subscribed: true, pullRequestNumber: number2 };
2190
2597
  }
2191
2598
  }),
2192
2599
  github_unsubscribe_pr: createTool({
@@ -2194,8 +2601,8 @@ function createGithubSubscriptionTools(requestContext, github) {
2194
2601
  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.",
2195
2602
  inputSchema: pullRequestInputSchema,
2196
2603
  execute: async ({ pullRequest }) => {
2197
- const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);
2198
- return { subscribed: false, pullRequestNumber: number };
2604
+ const number2 = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);
2605
+ return { subscribed: false, pullRequestNumber: number2 };
2199
2606
  }
2200
2607
  })
2201
2608
  };
@@ -3090,6 +3497,7 @@ var GithubIntegration = class {
3090
3497
  */
3091
3498
  routes(ctx) {
3092
3499
  this.#storage = ctx.storage;
3500
+ const ingestFactoryEvent = attachGithubRules(this, ctx);
3093
3501
  return buildGithubRoutes({
3094
3502
  github: this,
3095
3503
  auth: ctx.auth,
@@ -3101,7 +3509,7 @@ var GithubIntegration = class {
3101
3509
  runIssueTriage: ctx.controller ? (input) => runGithubIssueTriage({ controller: ctx.controller, input }) : void 0,
3102
3510
  emitAudit: ctx.hooks?.emitAudit,
3103
3511
  projects: ctx.storage.projects,
3104
- ingestFactoryEvent: ctx.hooks?.ingestGithubEvent
3512
+ ingestFactoryEvent
3105
3513
  });
3106
3514
  }
3107
3515
  /**