@mastra/factory 0.25.0-alpha.6 → 0.25.0-alpha.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"work-transition-policy.d.ts","sourceRoot":"","sources":["../../src/boards/work-transition-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAEpE,eAAO,MAAM,oBAAoB,EAAE,qBA+FlC,CAAC"}
1
+ {"version":3,"file":"work-transition-policy.d.ts","sourceRoot":"","sources":["../../src/boards/work-transition-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAEpE,eAAO,MAAM,oBAAoB,EAAE,qBAgGlC,CAAC"}
@@ -16,7 +16,7 @@ const workTransitionPolicy = (context) => {
16
16
  if ((fromStage === "planning" || skipsPlanning) && toStage === "execute" && actor.type !== "human" && !plansAutoApproved && !planApproved) return {
17
17
  type: "reject",
18
18
  code: "approval_required",
19
- reason: "Auto-approve plans is off: a maintainer must approve the plan or move this work item into Building from the Factory UI."
19
+ reason: "Auto-approve plans is off, so a person has to approve this plan. Ask them to say so in a chat they are signed in to (Slack, terminal, web); that turn can move the card into Building with factory_transition_work_item."
20
20
  };
21
21
  if (toStage === "done" && actor.type === "agent" && item.metadata?.reviewVerdict === "request changes") return {
22
22
  type: "reject",
@@ -31,7 +31,7 @@ const workTransitionPolicy = (context) => {
31
31
  if (fromStage === "intake" && actor.type === "agent" && item.metadata?.selfReviewNeedsHuman === true) return {
32
32
  type: "reject",
33
33
  code: "approval_required",
34
- reason: "Self-review requested changes too many times and parked this work item for a maintainer. A maintainer must move it on from the Factory UI."
34
+ reason: "Self-review requested changes too many times and parked this work item for a person. Ask them to decide in a chat they are signed in to; that turn can move the card on with factory_transition_work_item."
35
35
  };
36
36
  if (fromStage === "review" && toStage === "done" && actor.type === "agent" && typeof item.metadata?.openPullRequestNumber !== "number" && item.metadata?.reviewVerdict === void 0 && item.metadata?.selfReviewVerdict !== "approve") return {
37
37
  type: "reject",
@@ -43,7 +43,7 @@ const workTransitionPolicy = (context) => {
43
43
  if (triageType != null && triageType !== "bug" && entersWork && !isHumanTransition && !item.acceptedAt) return {
44
44
  type: "reject",
45
45
  code: "approval_required",
46
- reason: "A maintainer must move this non-bug work item into Planning or Execute from the Factory UI."
46
+ reason: "A person must accept this non-bug work item before it enters Planning or Execute. Ask them to say so in a chat they are signed in to; that turn can move the card with factory_transition_work_item."
47
47
  };
48
48
  return {
49
49
  type: "allow",
@@ -1 +1 @@
1
- {"version":3,"file":"work-transition-policy.js","names":[],"sources":["../../src/boards/work-transition-policy.ts"],"sourcesContent":["import type { BoardTransitionPolicy } from './transition-policy.js';\n\nexport const workTransitionPolicy: BoardTransitionPolicy = context => {\n const { item, requestedTriageType, actor, fromStage, toStage, isHumanTransition, plansAutoApproved, planApproved } =\n context;\n const triageAgent = actor.type === 'agent' && actor.role === 'triage';\n if (triageAgent && requestedTriageType === undefined) {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason: 'Triage transitions must report a structured triage classification.',\n };\n }\n if (item.triageType && requestedTriageType && item.triageType !== requestedTriageType) {\n return {\n type: 'reject',\n code: 'forbidden',\n reason: 'The persisted triage classification cannot be changed by a later transition.',\n };\n }\n // Planning is the approval boundary. When project auto-approval is off, only a\n // person or a successfully approved `submit_plan` result may move the card into Building.\n // An agent cannot skip Planning from Intake or Triage either; maintainer-configured\n // system rules may still route cards there.\n const skipsPlanning = (fromStage === 'intake' || fromStage === 'triage') && actor.type === 'agent';\n if (\n (fromStage === 'planning' || skipsPlanning) &&\n toStage === 'execute' &&\n actor.type !== 'human' &&\n !plansAutoApproved &&\n !planApproved\n ) {\n return {\n type: 'reject',\n code: 'approval_required',\n reason:\n 'Auto-approve plans is off: a maintainer must approve the plan or move this work item into Building from the Factory UI.',\n };\n }\n // A Factory review requested changes on this item's pull request. Closing the\n // work now would bury the finding; the next push re-reviews and updates it.\n if (toStage === 'done' && actor.type === 'agent' && item.metadata?.reviewVerdict === 'request changes') {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason:\n 'The latest Factory review requested changes on this pull request. Address them and push; the re-review updates the verdict. A merge or a maintainer can still close the work.',\n };\n }\n // The work ships when its pull request merges, and the merge closes the card.\n if (toStage === 'done' && actor.type === 'agent' && typeof item.metadata?.openPullRequestNumber === 'number') {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason: `Pull request #${item.metadata.openPullRequestNumber} is still open. The card moves to Done when it merges; a maintainer can still close the work.`,\n };\n }\n // Self-review hit its round limit and parked the card for a person.\n if (fromStage === 'intake' && actor.type === 'agent' && item.metadata?.selfReviewNeedsHuman === true) {\n return {\n type: 'reject',\n code: 'approval_required',\n reason:\n 'Self-review requested changes too many times and parked this work item for a maintainer. A maintainer must move it on from the Factory UI.',\n };\n }\n // In Review without a change request, the card is waiting on self-review; an agent cannot close it from there.\n if (\n fromStage === 'review' &&\n toStage === 'done' &&\n actor.type === 'agent' &&\n typeof item.metadata?.openPullRequestNumber !== 'number' &&\n item.metadata?.reviewVerdict === undefined &&\n item.metadata?.selfReviewVerdict !== 'approve'\n ) {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason:\n 'This work item is in self-review and has no open change request yet. Record a self-review verdict instead; a maintainer can still close the work.',\n };\n }\n const triageType = item.triageType ?? requestedTriageType;\n const entersWork = toStage === 'planning' || toStage === 'execute';\n // An intermediate phase is not evidence of human approval.\n if (triageType != null && triageType !== 'bug' && entersWork && !isHumanTransition && !item.acceptedAt) {\n return {\n type: 'reject',\n code: 'approval_required',\n reason: 'A maintainer must move this non-bug work item into Planning or Execute from the Factory UI.',\n };\n }\n return {\n type: 'allow',\n ...(triageAgent && requestedTriageType ? { triageType: requestedTriageType } : {}),\n ...(isHumanTransition && entersWork && !item.acceptedAt ? { accept: true as const } : {}),\n };\n};\n"],"mappings":";AAEA,MAAa,wBAA8C,YAAW;CACpE,MAAM,EAAE,MAAM,qBAAqB,OAAO,WAAW,SAAS,mBAAmB,mBAAmB,iBAClG;CACF,MAAM,cAAc,MAAM,SAAS,WAAW,MAAM,SAAS;CAC7D,IAAI,eAAe,wBAAwB,KAAA,GACzC,OAAO;EACL,MAAM;EACN,MAAM;EACN,QAAQ;CACV;CAEF,IAAI,KAAK,cAAc,uBAAuB,KAAK,eAAe,qBAChE,OAAO;EACL,MAAM;EACN,MAAM;EACN,QAAQ;CACV;CAMF,MAAM,iBAAiB,cAAc,YAAY,cAAc,aAAa,MAAM,SAAS;CAC3F,KACG,cAAc,cAAc,kBAC7B,YAAY,aACZ,MAAM,SAAS,WACf,CAAC,qBACD,CAAC,cAED,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAIF,IAAI,YAAY,UAAU,MAAM,SAAS,WAAW,KAAK,UAAU,kBAAkB,mBACnF,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAGF,IAAI,YAAY,UAAU,MAAM,SAAS,WAAW,OAAO,KAAK,UAAU,0BAA0B,UAClG,OAAO;EACL,MAAM;EACN,MAAM;EACN,QAAQ,iBAAiB,KAAK,SAAS,sBAAsB;CAC/D;CAGF,IAAI,cAAc,YAAY,MAAM,SAAS,WAAW,KAAK,UAAU,yBAAyB,MAC9F,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAGF,IACE,cAAc,YACd,YAAY,UACZ,MAAM,SAAS,WACf,OAAO,KAAK,UAAU,0BAA0B,YAChD,KAAK,UAAU,kBAAkB,KAAA,KACjC,KAAK,UAAU,sBAAsB,WAErC,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAEF,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,aAAa,YAAY,cAAc,YAAY;CAEzD,IAAI,cAAc,QAAQ,eAAe,SAAS,cAAc,CAAC,qBAAqB,CAAC,KAAK,YAC1F,OAAO;EACL,MAAM;EACN,MAAM;EACN,QAAQ;CACV;CAEF,OAAO;EACL,MAAM;EACN,GAAI,eAAe,sBAAsB,EAAE,YAAY,oBAAoB,IAAI,CAAC;EAChF,GAAI,qBAAqB,cAAc,CAAC,KAAK,aAAa,EAAE,QAAQ,KAAc,IAAI,CAAC;CACzF;AACF"}
1
+ {"version":3,"file":"work-transition-policy.js","names":[],"sources":["../../src/boards/work-transition-policy.ts"],"sourcesContent":["import type { BoardTransitionPolicy } from './transition-policy.js';\n\nexport const workTransitionPolicy: BoardTransitionPolicy = context => {\n const { item, requestedTriageType, actor, fromStage, toStage, isHumanTransition, plansAutoApproved, planApproved } =\n context;\n const triageAgent = actor.type === 'agent' && actor.role === 'triage';\n if (triageAgent && requestedTriageType === undefined) {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason: 'Triage transitions must report a structured triage classification.',\n };\n }\n if (item.triageType && requestedTriageType && item.triageType !== requestedTriageType) {\n return {\n type: 'reject',\n code: 'forbidden',\n reason: 'The persisted triage classification cannot be changed by a later transition.',\n };\n }\n // Planning is the approval boundary. When project auto-approval is off, only a\n // person or a successfully approved `submit_plan` result may move the card into Building.\n // An agent cannot skip Planning from Intake or Triage either; maintainer-configured\n // system rules may still route cards there.\n const skipsPlanning = (fromStage === 'intake' || fromStage === 'triage') && actor.type === 'agent';\n if (\n (fromStage === 'planning' || skipsPlanning) &&\n toStage === 'execute' &&\n actor.type !== 'human' &&\n !plansAutoApproved &&\n !planApproved\n ) {\n return {\n type: 'reject',\n code: 'approval_required',\n reason:\n 'Auto-approve plans is off, so a person has to approve this plan. Ask them to say so in a chat they are signed in to (Slack, terminal, web); that turn can move the card into Building with factory_transition_work_item.',\n };\n }\n // A Factory review requested changes on this item's pull request. Closing the\n // work now would bury the finding; the next push re-reviews and updates it.\n if (toStage === 'done' && actor.type === 'agent' && item.metadata?.reviewVerdict === 'request changes') {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason:\n 'The latest Factory review requested changes on this pull request. Address them and push; the re-review updates the verdict. A merge or a maintainer can still close the work.',\n };\n }\n // The work ships when its pull request merges, and the merge closes the card.\n if (toStage === 'done' && actor.type === 'agent' && typeof item.metadata?.openPullRequestNumber === 'number') {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason: `Pull request #${item.metadata.openPullRequestNumber} is still open. The card moves to Done when it merges; a maintainer can still close the work.`,\n };\n }\n // Self-review hit its round limit and parked the card for a person.\n if (fromStage === 'intake' && actor.type === 'agent' && item.metadata?.selfReviewNeedsHuman === true) {\n return {\n type: 'reject',\n code: 'approval_required',\n reason:\n 'Self-review requested changes too many times and parked this work item for a person. Ask them to decide in a chat they are signed in to; that turn can move the card on with factory_transition_work_item.',\n };\n }\n // In Review without a change request, the card is waiting on self-review; an agent cannot close it from there.\n if (\n fromStage === 'review' &&\n toStage === 'done' &&\n actor.type === 'agent' &&\n typeof item.metadata?.openPullRequestNumber !== 'number' &&\n item.metadata?.reviewVerdict === undefined &&\n item.metadata?.selfReviewVerdict !== 'approve'\n ) {\n return {\n type: 'reject',\n code: 'invalid_transition',\n reason:\n 'This work item is in self-review and has no open change request yet. Record a self-review verdict instead; a maintainer can still close the work.',\n };\n }\n const triageType = item.triageType ?? requestedTriageType;\n const entersWork = toStage === 'planning' || toStage === 'execute';\n // An intermediate phase is not evidence of human approval.\n if (triageType != null && triageType !== 'bug' && entersWork && !isHumanTransition && !item.acceptedAt) {\n return {\n type: 'reject',\n code: 'approval_required',\n reason:\n 'A person must accept this non-bug work item before it enters Planning or Execute. Ask them to say so in a chat they are signed in to; that turn can move the card with factory_transition_work_item.',\n };\n }\n return {\n type: 'allow',\n ...(triageAgent && requestedTriageType ? { triageType: requestedTriageType } : {}),\n ...(isHumanTransition && entersWork && !item.acceptedAt ? { accept: true as const } : {}),\n };\n};\n"],"mappings":";AAEA,MAAa,wBAA8C,YAAW;CACpE,MAAM,EAAE,MAAM,qBAAqB,OAAO,WAAW,SAAS,mBAAmB,mBAAmB,iBAClG;CACF,MAAM,cAAc,MAAM,SAAS,WAAW,MAAM,SAAS;CAC7D,IAAI,eAAe,wBAAwB,KAAA,GACzC,OAAO;EACL,MAAM;EACN,MAAM;EACN,QAAQ;CACV;CAEF,IAAI,KAAK,cAAc,uBAAuB,KAAK,eAAe,qBAChE,OAAO;EACL,MAAM;EACN,MAAM;EACN,QAAQ;CACV;CAMF,MAAM,iBAAiB,cAAc,YAAY,cAAc,aAAa,MAAM,SAAS;CAC3F,KACG,cAAc,cAAc,kBAC7B,YAAY,aACZ,MAAM,SAAS,WACf,CAAC,qBACD,CAAC,cAED,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAIF,IAAI,YAAY,UAAU,MAAM,SAAS,WAAW,KAAK,UAAU,kBAAkB,mBACnF,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAGF,IAAI,YAAY,UAAU,MAAM,SAAS,WAAW,OAAO,KAAK,UAAU,0BAA0B,UAClG,OAAO;EACL,MAAM;EACN,MAAM;EACN,QAAQ,iBAAiB,KAAK,SAAS,sBAAsB;CAC/D;CAGF,IAAI,cAAc,YAAY,MAAM,SAAS,WAAW,KAAK,UAAU,yBAAyB,MAC9F,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAGF,IACE,cAAc,YACd,YAAY,UACZ,MAAM,SAAS,WACf,OAAO,KAAK,UAAU,0BAA0B,YAChD,KAAK,UAAU,kBAAkB,KAAA,KACjC,KAAK,UAAU,sBAAsB,WAErC,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAEF,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,aAAa,YAAY,cAAc,YAAY;CAEzD,IAAI,cAAc,QAAQ,eAAe,SAAS,cAAc,CAAC,qBAAqB,CAAC,KAAK,YAC1F,OAAO;EACL,MAAM;EACN,MAAM;EACN,QACE;CACJ;CAEF,OAAO;EACL,MAAM;EACN,GAAI,eAAe,sBAAsB,EAAE,YAAY,oBAAoB,IAAI,CAAC;EAChF,GAAI,qBAAqB,cAAc,CAAC,KAAK,aAAa,EAAE,QAAQ,KAAc,IAAI,CAAC;CACzF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"channel-reply-processor.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/channel-reply-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAO/E,eAAO,MAAM,kBAAkB,wBAAmB,CAAC;AAYnD;;;;;;;;;;;;GAYG;AACH,qBAAa,4BAA6B,YAAW,SAAS;IAC5D,QAAQ,CAAC,EAAE,2BAAM;IAEjB,gBAAgB,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,oBAAoB;;;CAgC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,UAAU,EACV,gBAAgB,GACjB,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQxC;AAED,wBAAgB,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG9E"}
1
+ {"version":3,"file":"channel-reply-processor.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/channel-reply-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAQ/E,eAAO,MAAM,kBAAkB,wBAAmB,CAAC;AAanD;;;;;;;;;;;;GAYG;AACH,qBAAa,4BAA6B,YAAW,SAAS;IAC5D,QAAQ,CAAC,EAAE,2BAAM;IAEjB,gBAAgB,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,oBAAoB;;;CAgC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,UAAU,EACV,gBAAgB,GACjB,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQxC;AAED,wBAAgB,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE9E"}
@@ -1,3 +1,4 @@
1
+ import { factorySettingsUrl, factoryUiOrigin } from "../../links.js";
1
2
  // src/integrations/slack/channel-reply-processor.ts
2
3
  /** Mirrors `REPLY_TOOL_NAME` in `reply-tool.ts`, which imports this module. */
3
4
  const REPLY_TOOL = "reply_in_thread";
@@ -8,7 +9,11 @@ const SESSION_LINK_LABEL = "Open session ↗";
8
9
  * `FACT-526` or `52db47cd` is a dead end for the reader. Every channel run
9
10
  * gets this, whether it speaks in reply text or through the reply tool.
10
11
  */
11
- const LINK_REFERENCES = ["Write every pull request, issue, commit, Linear ticket or other reference you mention in the thread as a markdown link to its page, with the ID as plain link text: [#28467](https://github.com/<owner>/<repo>/pull/28467), [52db47cd](https://github.com/<owner>/<repo>/commit/52db47cd), [FACT-526](<the ticket URL>). Never leave one as a bare ID or wrap it in code formatting.", "Build GitHub and GitLab links from the repository the reference belongs to, from the repositories listed in your environment or a tool result; with more than one repository, don't assume which one a bare number belongs to. Build Linear links from the workspace URL pattern in the `linear_get_issue` tool description, or from a URL a tool returned. Don't guess a URL; leave a reference unlinked only when you can't work out its URL."].join("\n");
12
+ const LINK_REFERENCES = [
13
+ "Write every pull request, issue, commit, Linear ticket or other reference you mention in the thread as a markdown link to its page, with the ID as plain link text: [#28467](https://github.com/<owner>/<repo>/pull/28467), [52db47cd](https://github.com/<owner>/<repo>/commit/52db47cd), [FACT-526](<the ticket URL>). Never leave one as a bare ID or wrap it in code formatting.",
14
+ "Build GitHub and GitLab links from the repository the reference belongs to, from the repositories listed in your environment or a tool result; with more than one repository, don't assume which one a bare number belongs to. Build Linear links from the workspace URL pattern in the `linear_get_issue` tool description, or from a URL a tool returned. Don't guess a URL; leave a reference unlinked only when you can't work out its URL.",
15
+ "Link Factory work items and sessions the same way, with the `url` the Factory tools return: [#123](<item url>). When something needs a person, ask them for it here and link the item; only send them to the web UI when the channel cannot do it, and then link the exact page."
16
+ ].join("\n");
12
17
  /**
13
18
  * Tells the agent how to open a reply when the run came in over a chat
14
19
  * channel (Slack, …): acknowledge a job in one line that names it — "On it —
@@ -55,7 +60,7 @@ var FactoryChannelReplyProcessor = class {
55
60
  * query through.
56
61
  */
57
62
  function sessionLink({ threadId, sessionId, resourceId, factoryProjectId }) {
58
- const origin = process.env.MASTRACODE_PUBLIC_URL;
63
+ const origin = factoryUiOrigin();
59
64
  if (!origin) return void 0;
60
65
  const isChatOnly = resourceId.startsWith("channel:");
61
66
  const sessionParam = `?sessionId=${encodeURIComponent(sessionId)}`;
@@ -63,8 +68,7 @@ function sessionLink({ threadId, sessionId, resourceId, factoryProjectId }) {
63
68
  return `${origin}/factories/${encodeURIComponent(factoryProjectId)}/workspaces/${isChatOnly ? "channel" : encodeURIComponent(sessionId)}/threads/${encodeURIComponent(threadId)}${isChatOnly ? sessionParam : ""}`;
64
69
  }
65
70
  function modelSettingsLink(factoryProjectId) {
66
- const origin = process.env.MASTRACODE_PUBLIC_URL;
67
- return origin ? `${origin}/factories/${encodeURIComponent(factoryProjectId)}/settings/models` : void 0;
71
+ return factorySettingsUrl(factoryProjectId, "models");
68
72
  }
69
73
  export { FactoryChannelReplyProcessor, SESSION_LINK_LABEL, modelSettingsLink, sessionLink };
70
74
 
@@ -1 +1 @@
1
- {"version":3,"file":"channel-reply-processor.js","names":[],"sources":["../../../src/integrations/slack/channel-reply-processor.ts"],"sourcesContent":["import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';\n\n/** Mirrors `REPLY_TOOL_NAME` in `reply-tool.ts`, which imports this module. */\nconst REPLY_TOOL = 'reply_in_thread';\n\nconst ID = 'factory-channel-reply';\n\nexport const SESSION_LINK_LABEL = 'Open session ↗';\n\n/**\n * Chat surfaces show what the agent writes as-is, so a bare `#28467`,\n * `FACT-526` or `52db47cd` is a dead end for the reader. Every channel run\n * gets this, whether it speaks in reply text or through the reply tool.\n */\nconst LINK_REFERENCES = [\n 'Write every pull request, issue, commit, Linear ticket or other reference you mention in the thread as a markdown link to its page, with the ID as plain link text: [#28467](https://github.com/<owner>/<repo>/pull/28467), [52db47cd](https://github.com/<owner>/<repo>/commit/52db47cd), [FACT-526](<the ticket URL>). Never leave one as a bare ID or wrap it in code formatting.',\n \"Build GitHub and GitLab links from the repository the reference belongs to, from the repositories listed in your environment or a tool result; with more than one repository, don't assume which one a bare number belongs to. Build Linear links from the workspace URL pattern in the `linear_get_issue` tool description, or from a URL a tool returned. Don't guess a URL; leave a reference unlinked only when you can't work out its URL.\",\n].join('\\n');\n\n/**\n * Tells the agent how to open a reply when the run came in over a chat\n * channel (Slack, …): acknowledge a job in one line that names it — \"On it —\n * sleeping 30s, then running the echo\" — before starting, so the sender sees\n * a task-specific first message rather than a silent typing status until the\n * first tool finishes. The channel driver posts that opener on its own before\n * the tool widget, so the instruction and the rendering agree. The session\n * link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it\n * to the first reply of a new session, so it is there whether or not the\n * model judged the request worth an opener. A run on a Slack-linked session\n * that didn't start from Slack gets a shorter note on when to post there; runs\n * with no channel at all (Studio, API, boards) get nothing.\n */\nexport class FactoryChannelReplyProcessor implements Processor {\n readonly id = ID;\n\n processInputStep({ messageList, requestContext, tools }: ProcessInputStepArgs) {\n if (!requestContext?.get('channel')) {\n // A run on a Slack-linked session that didn't start from a Slack message\n // (a web prompt, a woken run) still has the reply tool; core's channel\n // prompt says the thread is linked, this says how to use it.\n if (!tools || !(REPLY_TOOL in tools)) return undefined;\n messageList.addSystem(\n {\n role: 'system',\n content: `Post to that thread with \\`${REPLY_TOOL}\\`. This request came from outside it, so don't post an opener or progress there; post the outcome when the people in the thread asked for this work or are waiting on it, or when you're asked to. Don't write a note about what you posted. When you open a pull request, its link is posted to this thread for you, and again when it merges, so don't post the link yourself.\\n${LINK_REFERENCES}`,\n },\n ID,\n );\n return { messageList };\n }\n // With reply text hidden the thread only sees what the reply tool posts,\n // so the opener advice becomes advice about when to call it.\n const content =\n tools && REPLY_TOOL in tools\n ? [\n `Only messages you send with \\`${REPLY_TOOL}\\` reach this thread; your other text stays in the session. Don't write a note about what you posted. After a reply, either keep working or end your turn with an empty message; any text you write after posting is wasted.`,\n `When a request will take tools or more than a moment, post one short line first that says what you are about to do, then do the work and post the outcome when you finish — not a replay of the steps.`,\n `Post again in between only when you need an answer or have news the people in the thread would want. When a message needs nothing from you, don't call it.`,\n `When you open a pull request, its link is posted to this thread for you, and again when it merges, so don't post the link yourself.`,\n ]\n : [\n 'When a request will take tools or more than a moment to answer, reply first with one short line that says what you are about to do — \"On it — <the task in a few words>.\" — then do the work; the opener posts as its own message before any progress is shown. Give direct answers and trivial replies without an opener.',\n 'Finish with the outcome, not a replay of the steps.',\n ];\n messageList.addSystem({ role: 'system', content: [...content, LINK_REFERENCES].join('\\n') }, ID);\n return { messageList };\n }\n}\n\nexport interface SessionLinkTarget {\n threadId: string;\n sessionId: string;\n resourceId: string;\n factoryProjectId?: string;\n}\n\n/**\n * The web URL for a session, or undefined without a public origin. Same\n * routes the Factory UI navigates to: a repo-backed thread's Factory session\n * id is the workspace segment; chat-only threads keep the literal `channel`\n * segment and name their session in the query; a session with no factory\n * falls back to the factory-agnostic /threads/ redirect, which forwards the\n * query through.\n */\nexport function sessionLink({\n threadId,\n sessionId,\n resourceId,\n factoryProjectId,\n}: SessionLinkTarget): string | undefined {\n const origin = process.env.MASTRACODE_PUBLIC_URL;\n if (!origin) return undefined;\n const isChatOnly = resourceId.startsWith('channel:');\n const sessionParam = `?sessionId=${encodeURIComponent(sessionId)}`;\n if (!factoryProjectId) return `${origin}/threads/${encodeURIComponent(threadId)}${sessionParam}`;\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(sessionId);\n return `${origin}/factories/${encodeURIComponent(factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(threadId)}${isChatOnly ? sessionParam : ''}`;\n}\n\nexport function modelSettingsLink(factoryProjectId: string): string | undefined {\n const origin = process.env.MASTRACODE_PUBLIC_URL;\n return origin ? `${origin}/factories/${encodeURIComponent(factoryProjectId)}/settings/models` : undefined;\n}\n"],"mappings":";;AAGA,MAAM,aAAa;AAEnB,MAAM,KAAK;AAEX,MAAa,qBAAqB;;;;;;AAOlC,MAAM,kBAAkB,CACtB,wXACA,ibACF,CAAC,CAAC,KAAK,IAAI;;;;;;;;;;;;;;AAeX,IAAa,+BAAb,MAA+D;CAC7D,KAAc;CAEd,iBAAiB,EAAE,aAAa,gBAAgB,SAA+B;EAC7E,IAAI,CAAC,gBAAgB,IAAI,SAAS,GAAG;GAInC,IAAI,CAAC,SAAS,EAAE,cAAc,QAAQ,OAAO,KAAA;GAC7C,YAAY,UACV;IACE,MAAM;IACN,SAAS,8BAA8B,WAAW,qXAAqX;GACza,GACA,EACF;GACA,OAAO,EAAE,YAAY;EACvB;EAGA,MAAM,UACJ,SAAS,cAAc,QACnB;GACE,iCAAiC,WAAW;GAC5C;GACA;GACA;EACF,IACA,CACE,gUACA,qDACF;EACN,YAAY,UAAU;GAAE,MAAM;GAAU,SAAS,CAAC,GAAG,SAAS,eAAe,CAAC,CAAC,KAAK,IAAI;EAAE,GAAG,EAAE;EAC/F,OAAO,EAAE,YAAY;CACvB;AACF;;;;;;;;;AAiBA,SAAgB,YAAY,EAC1B,UACA,WACA,YACA,oBACwC;CACxC,MAAM,SAAS,QAAQ,IAAI;CAC3B,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,aAAa,WAAW,WAAW,UAAU;CACnD,MAAM,eAAe,cAAc,mBAAmB,SAAS;CAC/D,IAAI,CAAC,kBAAkB,OAAO,GAAG,OAAO,WAAW,mBAAmB,QAAQ,IAAI;CAElF,OAAO,GAAG,OAAO,aAAa,mBAAmB,gBAAgB,EAAE,cAD1C,aAAa,YAAY,mBAAmB,SAAS,EACoB,WAAW,mBAAmB,QAAQ,IAAI,aAAa,eAAe;AAC1K;AAEA,SAAgB,kBAAkB,kBAA8C;CAC9E,MAAM,SAAS,QAAQ,IAAI;CAC3B,OAAO,SAAS,GAAG,OAAO,aAAa,mBAAmB,gBAAgB,EAAE,oBAAoB,KAAA;AAClG"}
1
+ {"version":3,"file":"channel-reply-processor.js","names":[],"sources":["../../../src/integrations/slack/channel-reply-processor.ts"],"sourcesContent":["import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';\nimport { factorySettingsUrl, factoryUiOrigin } from '../../links.js';\n\n/** Mirrors `REPLY_TOOL_NAME` in `reply-tool.ts`, which imports this module. */\nconst REPLY_TOOL = 'reply_in_thread';\n\nconst ID = 'factory-channel-reply';\n\nexport const SESSION_LINK_LABEL = 'Open session ↗';\n\n/**\n * Chat surfaces show what the agent writes as-is, so a bare `#28467`,\n * `FACT-526` or `52db47cd` is a dead end for the reader. Every channel run\n * gets this, whether it speaks in reply text or through the reply tool.\n */\nconst LINK_REFERENCES = [\n 'Write every pull request, issue, commit, Linear ticket or other reference you mention in the thread as a markdown link to its page, with the ID as plain link text: [#28467](https://github.com/<owner>/<repo>/pull/28467), [52db47cd](https://github.com/<owner>/<repo>/commit/52db47cd), [FACT-526](<the ticket URL>). Never leave one as a bare ID or wrap it in code formatting.',\n \"Build GitHub and GitLab links from the repository the reference belongs to, from the repositories listed in your environment or a tool result; with more than one repository, don't assume which one a bare number belongs to. Build Linear links from the workspace URL pattern in the `linear_get_issue` tool description, or from a URL a tool returned. Don't guess a URL; leave a reference unlinked only when you can't work out its URL.\",\n 'Link Factory work items and sessions the same way, with the `url` the Factory tools return: [#123](<item url>). When something needs a person, ask them for it here and link the item; only send them to the web UI when the channel cannot do it, and then link the exact page.',\n].join('\\n');\n\n/**\n * Tells the agent how to open a reply when the run came in over a chat\n * channel (Slack, …): acknowledge a job in one line that names it — \"On it —\n * sleeping 30s, then running the echo\" — before starting, so the sender sees\n * a task-specific first message rather than a silent typing status until the\n * first tool finishes. The channel driver posts that opener on its own before\n * the tool widget, so the instruction and the rendering agree. The session\n * link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it\n * to the first reply of a new session, so it is there whether or not the\n * model judged the request worth an opener. A run on a Slack-linked session\n * that didn't start from Slack gets a shorter note on when to post there; runs\n * with no channel at all (Studio, API, boards) get nothing.\n */\nexport class FactoryChannelReplyProcessor implements Processor {\n readonly id = ID;\n\n processInputStep({ messageList, requestContext, tools }: ProcessInputStepArgs) {\n if (!requestContext?.get('channel')) {\n // A run on a Slack-linked session that didn't start from a Slack message\n // (a web prompt, a woken run) still has the reply tool; core's channel\n // prompt says the thread is linked, this says how to use it.\n if (!tools || !(REPLY_TOOL in tools)) return undefined;\n messageList.addSystem(\n {\n role: 'system',\n content: `Post to that thread with \\`${REPLY_TOOL}\\`. This request came from outside it, so don't post an opener or progress there; post the outcome when the people in the thread asked for this work or are waiting on it, or when you're asked to. Don't write a note about what you posted. When you open a pull request, its link is posted to this thread for you, and again when it merges, so don't post the link yourself.\\n${LINK_REFERENCES}`,\n },\n ID,\n );\n return { messageList };\n }\n // With reply text hidden the thread only sees what the reply tool posts,\n // so the opener advice becomes advice about when to call it.\n const content =\n tools && REPLY_TOOL in tools\n ? [\n `Only messages you send with \\`${REPLY_TOOL}\\` reach this thread; your other text stays in the session. Don't write a note about what you posted. After a reply, either keep working or end your turn with an empty message; any text you write after posting is wasted.`,\n `When a request will take tools or more than a moment, post one short line first that says what you are about to do, then do the work and post the outcome when you finish — not a replay of the steps.`,\n `Post again in between only when you need an answer or have news the people in the thread would want. When a message needs nothing from you, don't call it.`,\n `When you open a pull request, its link is posted to this thread for you, and again when it merges, so don't post the link yourself.`,\n ]\n : [\n 'When a request will take tools or more than a moment to answer, reply first with one short line that says what you are about to do — \"On it — <the task in a few words>.\" — then do the work; the opener posts as its own message before any progress is shown. Give direct answers and trivial replies without an opener.',\n 'Finish with the outcome, not a replay of the steps.',\n ];\n messageList.addSystem({ role: 'system', content: [...content, LINK_REFERENCES].join('\\n') }, ID);\n return { messageList };\n }\n}\n\nexport interface SessionLinkTarget {\n threadId: string;\n sessionId: string;\n resourceId: string;\n factoryProjectId?: string;\n}\n\n/**\n * The web URL for a session, or undefined without a public origin. Same\n * routes the Factory UI navigates to: a repo-backed thread's Factory session\n * id is the workspace segment; chat-only threads keep the literal `channel`\n * segment and name their session in the query; a session with no factory\n * falls back to the factory-agnostic /threads/ redirect, which forwards the\n * query through.\n */\nexport function sessionLink({\n threadId,\n sessionId,\n resourceId,\n factoryProjectId,\n}: SessionLinkTarget): string | undefined {\n const origin = factoryUiOrigin();\n if (!origin) return undefined;\n const isChatOnly = resourceId.startsWith('channel:');\n const sessionParam = `?sessionId=${encodeURIComponent(sessionId)}`;\n if (!factoryProjectId) return `${origin}/threads/${encodeURIComponent(threadId)}${sessionParam}`;\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(sessionId);\n return `${origin}/factories/${encodeURIComponent(factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(threadId)}${isChatOnly ? sessionParam : ''}`;\n}\n\nexport function modelSettingsLink(factoryProjectId: string): string | undefined {\n return factorySettingsUrl(factoryProjectId, 'models');\n}\n"],"mappings":";;;AAIA,MAAM,aAAa;AAEnB,MAAM,KAAK;AAEX,MAAa,qBAAqB;;;;;;AAOlC,MAAM,kBAAkB;CACtB;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;;;;;;;;;;;;;;AAeX,IAAa,+BAAb,MAA+D;CAC7D,KAAc;CAEd,iBAAiB,EAAE,aAAa,gBAAgB,SAA+B;EAC7E,IAAI,CAAC,gBAAgB,IAAI,SAAS,GAAG;GAInC,IAAI,CAAC,SAAS,EAAE,cAAc,QAAQ,OAAO,KAAA;GAC7C,YAAY,UACV;IACE,MAAM;IACN,SAAS,8BAA8B,WAAW,qXAAqX;GACza,GACA,EACF;GACA,OAAO,EAAE,YAAY;EACvB;EAGA,MAAM,UACJ,SAAS,cAAc,QACnB;GACE,iCAAiC,WAAW;GAC5C;GACA;GACA;EACF,IACA,CACE,gUACA,qDACF;EACN,YAAY,UAAU;GAAE,MAAM;GAAU,SAAS,CAAC,GAAG,SAAS,eAAe,CAAC,CAAC,KAAK,IAAI;EAAE,GAAG,EAAE;EAC/F,OAAO,EAAE,YAAY;CACvB;AACF;;;;;;;;;AAiBA,SAAgB,YAAY,EAC1B,UACA,WACA,YACA,oBACwC;CACxC,MAAM,SAAS,gBAAgB;CAC/B,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,aAAa,WAAW,WAAW,UAAU;CACnD,MAAM,eAAe,cAAc,mBAAmB,SAAS;CAC/D,IAAI,CAAC,kBAAkB,OAAO,GAAG,OAAO,WAAW,mBAAmB,QAAQ,IAAI;CAElF,OAAO,GAAG,OAAO,aAAa,mBAAmB,gBAAgB,EAAE,cAD1C,aAAa,YAAY,mBAAmB,SAAS,EACoB,WAAW,mBAAmB,QAAQ,IAAI,aAAa,eAAe;AAC1K;AAEA,SAAgB,kBAAkB,kBAA8C;CAC9E,OAAO,mBAAmB,kBAAkB,QAAQ;AACtD"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Web UI links to Factory entities, so channels (Slack, TUI, GitHub) can point people at the exact
3
+ * thing instead of telling them to "go use the UI". Every builder returns undefined when
4
+ * `MASTRACODE_PUBLIC_URL` is unset or blank: an unconfigured server has no address to link to.
5
+ */
6
+ /** The configured web UI origin, trimmed and without a trailing slash; null when unset or blank. */
7
+ export declare function factoryUiOrigin(raw?: string | undefined): string | null;
8
+ export declare function factoryBoardUrl(factoryProjectId: string, board: string): string | undefined;
9
+ /** A card opened on its board, optionally scrolled to one feed comment. */
10
+ export declare function factoryWorkItemUrl(input: {
11
+ factoryProjectId: string;
12
+ board: string;
13
+ workItemId: string;
14
+ commentId?: string;
15
+ }): string | undefined;
16
+ export declare function factorySessionUrl(input: {
17
+ factoryProjectId: string;
18
+ sessionId: string;
19
+ threadId: string;
20
+ }): string | undefined;
21
+ /** A settings page, e.g. `models`, `general`, or `connections/slack`. */
22
+ export declare function factorySettingsUrl(factoryProjectId: string, section: string): string | undefined;
23
+ export declare function factoryRulesUrl(factoryProjectId: string, group?: string): string | undefined;
24
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,oGAAoG;AACpG,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,GAAG,SAA6C,GAAG,MAAM,GAAG,IAAI,CAE1G;AAYD,wBAAgB,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE3F;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,GAAG,SAAS,CAIrB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,GAAG,SAAS,CAKrB;AAED,yEAAyE;AACzE,wBAAgB,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEhG;AAED,wBAAgB,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE5F"}
package/dist/links.js ADDED
@@ -0,0 +1,40 @@
1
+ // src/links.ts
2
+ /**
3
+ * Web UI links to Factory entities, so channels (Slack, TUI, GitHub) can point people at the exact
4
+ * thing instead of telling them to "go use the UI". Every builder returns undefined when
5
+ * `MASTRACODE_PUBLIC_URL` is unset or blank: an unconfigured server has no address to link to.
6
+ */
7
+ /** The configured web UI origin, trimmed and without a trailing slash; null when unset or blank. */
8
+ function factoryUiOrigin(raw = process.env.MASTRACODE_PUBLIC_URL) {
9
+ return raw?.trim().replace(/\/+$/, "") || null;
10
+ }
11
+ function factoryUrl(factoryProjectId, path) {
12
+ const origin = factoryUiOrigin();
13
+ return origin ? `${origin}/factories/${encodeURIComponent(factoryProjectId)}${path}` : void 0;
14
+ }
15
+ /** Board path segment: the built-in boards have their own routes, others live under /boards/:id. */
16
+ function boardSegment(board) {
17
+ return board === "work" || board === "review" ? `/${board}` : `/boards/${encodeURIComponent(board)}`;
18
+ }
19
+ function factoryBoardUrl(factoryProjectId, board) {
20
+ return factoryUrl(factoryProjectId, boardSegment(board));
21
+ }
22
+ /** A card opened on its board, optionally scrolled to one feed comment. */
23
+ function factoryWorkItemUrl(input) {
24
+ const query = new URLSearchParams({ item: input.workItemId });
25
+ if (input.commentId) query.set("comment", input.commentId);
26
+ return factoryUrl(input.factoryProjectId, `${boardSegment(input.board)}?${query}`);
27
+ }
28
+ function factorySessionUrl(input) {
29
+ return factoryUrl(input.factoryProjectId, `/workspaces/${encodeURIComponent(input.sessionId)}/threads/${encodeURIComponent(input.threadId)}`);
30
+ }
31
+ /** A settings page, e.g. `models`, `general`, or `connections/slack`. */
32
+ function factorySettingsUrl(factoryProjectId, section) {
33
+ return factoryUrl(factoryProjectId, `/settings/${section.split("/").map(encodeURIComponent).join("/")}`);
34
+ }
35
+ function factoryRulesUrl(factoryProjectId, group) {
36
+ return factoryUrl(factoryProjectId, group ? `/rules?${new URLSearchParams({ group })}` : "/rules");
37
+ }
38
+ export { factoryBoardUrl, factoryRulesUrl, factorySessionUrl, factorySettingsUrl, factoryUiOrigin, factoryWorkItemUrl };
39
+
40
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","names":[],"sources":["../src/links.ts"],"sourcesContent":["/**\n * Web UI links to Factory entities, so channels (Slack, TUI, GitHub) can point people at the exact\n * thing instead of telling them to \"go use the UI\". Every builder returns undefined when\n * `MASTRACODE_PUBLIC_URL` is unset or blank: an unconfigured server has no address to link to.\n */\n\n/** The configured web UI origin, trimmed and without a trailing slash; null when unset or blank. */\nexport function factoryUiOrigin(raw: string | undefined = process.env.MASTRACODE_PUBLIC_URL): string | null {\n return raw?.trim().replace(/\\/+$/, '') || null;\n}\n\nfunction factoryUrl(factoryProjectId: string, path: string): string | undefined {\n const origin = factoryUiOrigin();\n return origin ? `${origin}/factories/${encodeURIComponent(factoryProjectId)}${path}` : undefined;\n}\n\n/** Board path segment: the built-in boards have their own routes, others live under /boards/:id. */\nfunction boardSegment(board: string): string {\n return board === 'work' || board === 'review' ? `/${board}` : `/boards/${encodeURIComponent(board)}`;\n}\n\nexport function factoryBoardUrl(factoryProjectId: string, board: string): string | undefined {\n return factoryUrl(factoryProjectId, boardSegment(board));\n}\n\n/** A card opened on its board, optionally scrolled to one feed comment. */\nexport function factoryWorkItemUrl(input: {\n factoryProjectId: string;\n board: string;\n workItemId: string;\n commentId?: string;\n}): string | undefined {\n const query = new URLSearchParams({ item: input.workItemId });\n if (input.commentId) query.set('comment', input.commentId);\n return factoryUrl(input.factoryProjectId, `${boardSegment(input.board)}?${query}`);\n}\n\nexport function factorySessionUrl(input: {\n factoryProjectId: string;\n sessionId: string;\n threadId: string;\n}): string | undefined {\n return factoryUrl(\n input.factoryProjectId,\n `/workspaces/${encodeURIComponent(input.sessionId)}/threads/${encodeURIComponent(input.threadId)}`,\n );\n}\n\n/** A settings page, e.g. `models`, `general`, or `connections/slack`. */\nexport function factorySettingsUrl(factoryProjectId: string, section: string): string | undefined {\n return factoryUrl(factoryProjectId, `/settings/${section.split('/').map(encodeURIComponent).join('/')}`);\n}\n\nexport function factoryRulesUrl(factoryProjectId: string, group?: string): string | undefined {\n return factoryUrl(factoryProjectId, group ? `/rules?${new URLSearchParams({ group })}` : '/rules');\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,gBAAgB,MAA0B,QAAQ,IAAI,uBAAsC;CAC1G,OAAO,KAAK,KAAK,CAAC,CAAC,QAAQ,QAAQ,EAAE,KAAK;AAC5C;AAEA,SAAS,WAAW,kBAA0B,MAAkC;CAC9E,MAAM,SAAS,gBAAgB;CAC/B,OAAO,SAAS,GAAG,OAAO,aAAa,mBAAmB,gBAAgB,IAAI,SAAS,KAAA;AACzF;;AAGA,SAAS,aAAa,OAAuB;CAC3C,OAAO,UAAU,UAAU,UAAU,WAAW,IAAI,UAAU,WAAW,mBAAmB,KAAK;AACnG;AAEA,SAAgB,gBAAgB,kBAA0B,OAAmC;CAC3F,OAAO,WAAW,kBAAkB,aAAa,KAAK,CAAC;AACzD;;AAGA,SAAgB,mBAAmB,OAKZ;CACrB,MAAM,QAAQ,IAAI,gBAAgB,EAAE,MAAM,MAAM,WAAW,CAAC;CAC5D,IAAI,MAAM,WAAW,MAAM,IAAI,WAAW,MAAM,SAAS;CACzD,OAAO,WAAW,MAAM,kBAAkB,GAAG,aAAa,MAAM,KAAK,EAAE,GAAG,OAAO;AACnF;AAEA,SAAgB,kBAAkB,OAIX;CACrB,OAAO,WACL,MAAM,kBACN,eAAe,mBAAmB,MAAM,SAAS,EAAE,WAAW,mBAAmB,MAAM,QAAQ,GACjG;AACF;;AAGA,SAAgB,mBAAmB,kBAA0B,SAAqC;CAChG,OAAO,WAAW,kBAAkB,aAAa,QAAQ,MAAM,GAAG,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,KAAK,GAAG,GAAG;AACzG;AAEA,SAAgB,gBAAgB,kBAA0B,OAAoC;CAC5F,OAAO,WAAW,kBAAkB,QAAQ,UAAU,IAAI,gBAAgB,EAAE,MAAM,CAAC,MAAM,QAAQ;AACnG"}
@@ -1 +1 @@
1
- {"version":3,"file":"read-tools.d.ts","sourceRoot":"","sources":["../../src/tools/read-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,WAAW,EACX,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAG/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,eAAO,MAAM,QAAQ,KAAK,CAAC;AAG3B,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,uBAAuB,GACvB,WAAW,GACX,6BAA6B,GAC7B,yBAAyB,GACzB,0BAA0B,CAC7B,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAC,CAAC;IACtD,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAClC,MAAM,EAAE,aAAa,CAAC;IACtB,mHAAmH;IACnH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;CACxE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAQD,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CAGjF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,MAAM,CAEvE;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,aAOyB,CAAC;AAExD,+GAA+G;AAC/G,wBAAsB,YAAY,CAChC,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,eAAe,CAAC,EAC3D,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,kBAAkB,CAAC,EAC3D,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ7B;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAEnD;AAkED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,2BAA2B,GAAG,gBAAgB,CAsNnH"}
1
+ {"version":3,"file":"read-tools.d.ts","sourceRoot":"","sources":["../../src/tools/read-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAIhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,WAAW,EACX,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAG/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,eAAO,MAAM,QAAQ,KAAK,CAAC;AAG3B,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,uBAAuB,GACvB,WAAW,GACX,6BAA6B,GAC7B,yBAAyB,GACzB,0BAA0B,CAC7B,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAC,CAAC;IACtD,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAClC,MAAM,EAAE,aAAa,CAAC;IACtB,mHAAmH;IACnH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;CACxE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAQD,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CAGjF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,MAAM,CAEvE;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,aAOyB,CAAC;AAExD,+GAA+G;AAC/G,wBAAsB,YAAY,CAChC,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,eAAe,CAAC,EAC3D,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,kBAAkB,CAAC,EAC3D,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ7B;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAEnD;AA8ED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,2BAA2B,GAAG,gBAAgB,CA6NnH"}
@@ -2,6 +2,7 @@ import { boardForWorkItem } from "../boards/semantics.js";
2
2
  import "../boards/index.js";
3
3
  import { factoryDispatchFailureMetadata } from "../rules/dispatch-errors.js";
4
4
  import { sessionActivity } from "./deliver.js";
5
+ import { factorySessionUrl, factoryWorkItemUrl } from "../links.js";
5
6
  import { decisionRetryable, overtakenDecisionIds } from "../rules/decision-applicability.js";
6
7
  import { z } from "zod";
7
8
  import { createTool } from "@mastra/core/tools";
@@ -46,6 +47,14 @@ function activeWorker(bindings, itemId) {
46
47
  sessionId: binding.sessionId
47
48
  } : null;
48
49
  }
50
+ /** The card on its Factory board; null when the server has no public URL configured. */
51
+ function workItemUrl(item) {
52
+ return factoryWorkItemUrl({
53
+ factoryProjectId: item.factoryProjectId,
54
+ board: boardForWorkItem(item),
55
+ workItemId: item.id
56
+ }) ?? null;
57
+ }
49
58
  function summarizeItem(item, bindings) {
50
59
  return {
51
60
  id: item.id,
@@ -53,6 +62,7 @@ function summarizeItem(item, bindings) {
53
62
  title: truncate(item.title, 600),
54
63
  board: boardForWorkItem(item),
55
64
  stage: workItemStage(item),
65
+ url: workItemUrl(item),
56
66
  activeWorker: activeWorker(bindings, item.id),
57
67
  requestedBy: item.createdBy,
58
68
  updatedAt: iso(item.updatedAt)
@@ -176,6 +186,11 @@ function createFactoryReadTools(scope, deps) {
176
186
  role,
177
187
  sessionId: ref.sessionId,
178
188
  threadId: ref.threadId,
189
+ url: factorySessionUrl({
190
+ factoryProjectId: item.factoryProjectId,
191
+ sessionId: ref.sessionId,
192
+ threadId: ref.threadId
193
+ }) ?? null,
179
194
  startedBy: ref.startedBy,
180
195
  active: bindings.some((binding) => binding.role === role && binding.sessionId === ref.sessionId && binding.status === "active"),
181
196
  ...await activity(ref.sessionId, ref.threadId) ?? {
@@ -187,7 +202,7 @@ function createFactoryReadTools(scope, deps) {
187
202
  item: {
188
203
  ...summarizeItem(item, bindings),
189
204
  source: item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : "manual",
190
- url: typeof item.metadata?.url === "string" ? item.metadata.url : null,
205
+ sourceUrl: typeof item.metadata?.url === "string" ? item.metadata.url : null,
191
206
  triageType: item.triageType,
192
207
  revision: item.revision
193
208
  },
@@ -248,7 +263,8 @@ function createFactoryReadTools(scope, deps) {
248
263
  return {
249
264
  workItemId,
250
265
  number: item ? workItemNumber(item) : null,
251
- title: item ? truncate(item.title, 600) : null
266
+ title: item ? truncate(item.title, 600) : null,
267
+ url: item ? workItemUrl(item) : null
252
268
  };
253
269
  };
254
270
  const allIncidents = [...groups.values()].sort((a, b) => b.decisions.length - a.decisions.length);
@@ -1 +1 @@
1
- {"version":3,"file":"read-tools.js","names":[],"sources":["../../src/tools/read-tools.ts"],"sourcesContent":["import { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { decisionRetryable, overtakenDecisionIds } from '../rules/decision-applicability.js';\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type { AuditStorage } from '../storage/domains/audit/base.js';\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { sessionActivity } from './deliver.js';\nimport type { FactorySessionActivity } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport const MAX_TEXT = 600;\nexport const MAX_ERROR = 400;\nexport const MAX_LIST = 50;\nconst DEFAULT_LIST = 25;\n\nexport interface FactoryReadToolDependencies {\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'listDeferredDecisions'\n | 'listByIds'\n | 'listDecisionsForEvaluations'\n | 'listWorkItemSubscribers'\n | 'listSessionSubscriptions'\n >;\n comments: Pick<WorkItemCommentsStorage, 'listRecent'>;\n audit: Pick<AuditStorage, 'list'>;\n boards: BoardRegistry;\n /** The live controller, for whether a session is mid-run and when it last moved; absent before the mount is up. */\n messaging?: () => Promise<Pick<FactoryMessaging, 'controller'> | null>;\n}\n\nexport function truncate(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nfunction iso(value: Date | string | null | undefined): string | null {\n if (!value) return null;\n const date = value instanceof Date ? value : new Date(value);\n return Number.isFinite(date.getTime()) ? date.toISOString() : null;\n}\n\n/** The number a person sees on the card: the issue or PR number the item was filed from. */\nexport function workItemNumber(item: Pick<WorkItemRow, 'metadata'>): number | null {\n const number = item.metadata?.number ?? item.metadata?.githubIssueNumber ?? item.metadata?.githubPullRequestNumber;\n return typeof number === 'number' ? number : null;\n}\n\nexport function workItemStage(item: Pick<WorkItemRow, 'stages'>): string {\n return item.stages.join('+');\n}\n\n/** A work item id or its card number; anything else can't name an item. */\nexport const workItemRefSchema = z\n .string()\n .trim()\n .max(64)\n .refine(ref => z.string().uuid().safeParse(ref).success || /^#?\\d+$/.test(ref), {\n message: 'Use a work item id (UUID) or its number like \"#123\".',\n })\n .describe('Work item id, or its number like \"#123\".');\n\n/** Resolve `<uuid>` or `#123` to an item in this Factory only. Pass a ref validated by `workItemRefSchema`. */\nexport async function findWorkItem(\n workItems: Pick<WorkItemsStorage, 'list' | 'getForProject'>,\n scope: Pick<FactoryToolScope, 'orgId' | 'factoryProjectId'>,\n ref: string,\n): Promise<WorkItemRow | null> {\n const trimmed = ref.trim();\n const number = /^#?(\\d+)$/.exec(trimmed)?.[1];\n if (number === undefined) return workItems.getForProject(scope.orgId, scope.factoryProjectId, trimmed);\n const items = await workItems.list(scope);\n const matches = items.filter(item => workItemNumber(item) === Number(number));\n // Issue and PR numbers share a space on GitHub; prefer the Work-board card.\n return matches.find(item => item.externalSource?.type !== 'pull-request') ?? matches[0] ?? null;\n}\n\nexport function workItemNotFound(ref: string): Error {\n return new Error(`No work item ${ref} was found in this Factory.`);\n}\n\nfunction activeWorker(bindings: FactoryRunBindingRecord[], itemId: string) {\n const binding = bindings.find(candidate => candidate.workItemId === itemId && candidate.status === 'active');\n return binding ? { role: binding.role, sessionId: binding.sessionId } : null;\n}\n\nfunction summarizeItem(item: WorkItemRow, bindings: FactoryRunBindingRecord[]) {\n return {\n id: item.id,\n number: workItemNumber(item),\n title: truncate(item.title, MAX_TEXT),\n board: boardForWorkItem(item),\n stage: workItemStage(item),\n activeWorker: activeWorker(bindings, item.id),\n requestedBy: item.createdBy,\n updatedAt: iso(item.updatedAt),\n };\n}\n\nfunction summarizeDecision(decision: FactoryDeferredDecisionRecord, overtaken = false) {\n const type = typeof decision.decision.type === 'string' ? decision.decision.type : 'decision';\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : null;\n return {\n id: decision.id,\n workItemId: decision.workItemId,\n type,\n role,\n status: decision.status,\n attempts: decision.attempts,\n failureCode: decision.failureCode,\n failureLabel: decision.failureCode ? factoryDispatchFailureMetadata(decision.failureCode).label : null,\n canRetry: decisionRetryable(decision, overtaken),\n error: decision.lastError ? truncate(decision.lastError, MAX_ERROR) : null,\n approvedBy: decision.approvedBy,\n updatedAt: iso(decision.updatedAt),\n };\n}\n\n/** Held: a non-bug item triage parked until a person accepts it. */\nfunction isHeld(item: WorkItemRow): boolean {\n return item.stages.includes('triage') && !!item.triageType && item.triageType !== 'bug' && !item.acceptedAt;\n}\n\nconst listInputSchema = z\n .object({\n query: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Case-insensitive text in the title, or an exact card number like \"#123\".'),\n stage: z.string().min(1).max(64).optional().describe('Only items in this stage, e.g. \"triage\" or \"review\".'),\n board: z.string().min(1).max(64).optional().describe('Only items on this board, e.g. \"work\" or \"review\".'),\n role: z.string().min(1).max(64).optional().describe('Only items that have a session for this role.'),\n active: z.boolean().optional().describe('true: only items with an agent working now; false: only idle items.'),\n requestedBy: z.literal('me').optional().describe('\"me\": only items you filed.'),\n subscribed: z\n .boolean()\n .optional()\n .describe('true: only items this session follows; false: only items it does not.'),\n limit: z.number().int().min(1).max(MAX_LIST).optional().describe(`Default ${DEFAULT_LIST}, max ${MAX_LIST}.`),\n })\n .strict();\n\nexport function createFactoryReadTools(scope: FactoryToolScope, deps: FactoryReadToolDependencies): IntegrationTools {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n\n return {\n factory_list_work_items: createTool({\n id: 'factory_list_work_items',\n description:\n \"List this Factory's work items, newest activity first, with counts per stage for the whole Factory. Filter by text or #number, stage, board, role, whether an agent is working on it, items you filed, or items this session follows. Check here for an existing item before filing a new one.\",\n inputSchema: listInputSchema,\n execute: async input => {\n const [items, bindings, followed] = await Promise.all([\n deps.workItems.list(project),\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId),\n input.subscribed === undefined\n ? undefined\n : deps.workItems.listSessionSubscriptions({ ...project, sessionId: scope.sessionId }),\n ]);\n const followedIds = new Set(followed?.map(subscription => subscription.workItemId));\n const byStage: Record<string, number> = {};\n for (const item of items) {\n const stage = workItemStage(item);\n byStage[stage] = (byStage[stage] ?? 0) + 1;\n }\n const numberQuery = input.query ? /^#(\\d+)$/.exec(input.query)?.[1] : undefined;\n const text = input.query?.toLowerCase();\n const matches = items\n .filter(item => {\n if (numberQuery !== undefined) {\n if (workItemNumber(item) !== Number(numberQuery)) return false;\n } else if (text && !item.title.toLowerCase().includes(text)) return false;\n if (input.stage && !item.stages.includes(input.stage)) return false;\n if (input.board && boardForWorkItem(item) !== input.board) return false;\n if (input.role && !item.sessions[input.role]) return false;\n if (input.active !== undefined && (activeWorker(bindings, item.id) !== null) !== input.active) return false;\n if (input.requestedBy === 'me' && item.createdBy !== scope.userId) return false;\n if (input.subscribed !== undefined && followedIds.has(item.id) !== input.subscribed) return false;\n return true;\n })\n .sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime());\n const limit = input.limit ?? DEFAULT_LIST;\n return {\n counts: { total: items.length, byStage },\n items: matches.slice(0, limit).map(item => summarizeItem(item, bindings)),\n truncated: matches.length > limit,\n };\n },\n }),\n\n factory_inspect_work_item: createTool({\n id: 'factory_inspect_work_item',\n description:\n 'Everything the Factory knows about one work item: stage history, sessions by role (with whether each is mid-run and when its thread last moved), decisions with errors, recent audit events, recent feed comments, labels, linked items and the sessions following it. Use it to explain why an item is where it is.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const [bindings, decisions, audit, feed, all, subscribers] = await Promise.all([\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id),\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, { workItemId: item.id }),\n deps.audit.list({ ...project, limit: 200 }),\n deps.comments.listRecent({ ...project, workItemId: item.id, limit: 10 }),\n deps.workItems.list(project),\n deps.workItems.listWorkItemSubscribers({ orgId: scope.orgId, workItemId: item.id }),\n ]);\n const itemDecisions = decisions.slice(-MAX_LIST);\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n deps.boards,\n project,\n itemDecisions.filter(decision => decision.status === 'failed'),\n );\n const labels = Array.isArray(item.metadata?.labels)\n ? item.metadata.labels.filter((label): label is string => typeof label === 'string').slice(0, MAX_LIST)\n : [];\n const parent = item.parentWorkItemId\n ? all.find(candidate => candidate.id === item.parentWorkItemId)\n : undefined;\n const children = all.filter(candidate => candidate.parentWorkItemId === item.id);\n const controller = (await deps.messaging?.())?.controller;\n const activity = async (sessionId: string, threadId: string): Promise<FactorySessionActivity | null> =>\n controller ? sessionActivity(controller, sessionId, threadId) : null;\n const sessions = await Promise.all(\n Object.entries(item.sessions).map(async ([role, ref]) => ({\n role,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n startedBy: ref.startedBy,\n active: bindings.some(\n binding => binding.role === role && binding.sessionId === ref.sessionId && binding.status === 'active',\n ),\n // `running` with an old `lastActivityAt` is a stuck run; `running` with a recent one is a long one.\n ...((await activity(ref.sessionId, ref.threadId)) ?? { running: null, lastActivityAt: null }),\n })),\n );\n return {\n item: {\n ...summarizeItem(item, bindings),\n source: item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : 'manual',\n url: typeof item.metadata?.url === 'string' ? item.metadata.url : null,\n triageType: item.triageType,\n revision: item.revision,\n },\n labels,\n stageHistory: item.stageHistory.slice(-MAX_LIST).map(entry => ({\n stage: entry.stage,\n enteredAt: entry.enteredAt,\n exitedAt: entry.exitedAt ?? null,\n by: entry.by,\n })),\n sessions,\n decisions: itemDecisions.map(decision => summarizeDecision(decision, overtaken.has(decision.id))),\n audit: audit.events\n .filter(event => event.targets.some(target => target.type === 'work_item' && target.id === item.id))\n .slice(0, 25)\n .map(event => ({\n action: event.action,\n actorId: event.actorId,\n actorType: event.actorType,\n occurredAt: iso(event.occurredAt),\n })),\n feed: feed.map(comment => ({\n id: comment.id,\n kind: comment.kind,\n author: comment.author,\n occurredAt: iso(comment.occurredAt),\n body: truncate(comment.body, MAX_TEXT),\n })),\n parent: parent ? summarizeItem(parent, bindings) : null,\n children: children.slice(0, MAX_LIST).map(child => summarizeItem(child, bindings)),\n subscribers: subscribers.slice(0, MAX_LIST).map(subscription => ({\n sessionId: subscription.sessionId,\n reason: subscription.reason,\n isCaller: subscription.sessionId === scope.sessionId,\n })),\n };\n },\n }),\n\n factory_list_attention: createTool({\n id: 'factory_list_attention',\n description:\n 'What in this Factory needs a person: failed decisions grouped by failure code and error (one incident per group), proposals waiting for approval, and items triage is holding until someone accepts them.',\n inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_LIST).optional() }).strict(),\n execute: async ({ limit = DEFAULT_LIST }) => {\n const [decisions, items] = await Promise.all([\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, {\n statuses: ['failed', 'proposed'],\n }),\n deps.workItems.list(project),\n ]);\n const itemsById = new Map(items.map(item => [item.id, item]));\n const failed = decisions.filter(decision => decision.status === 'failed');\n const groups = new Map<\n string,\n { failureCode: string; error: string; decisions: FactoryDeferredDecisionRecord[] }\n >();\n for (const decision of failed) {\n const error = truncate(decision.lastError ?? '', MAX_ERROR);\n const key = `${decision.failureCode ?? 'unknown'}|${error}`;\n const group = groups.get(key) ?? { failureCode: decision.failureCode ?? 'unknown', error, decisions: [] };\n group.decisions.push(decision);\n groups.set(key, group);\n }\n const ref = (workItemId: string | null) => {\n const item = workItemId ? itemsById.get(workItemId) : undefined;\n return {\n workItemId,\n number: item ? workItemNumber(item) : null,\n title: item ? truncate(item.title, MAX_TEXT) : null,\n };\n };\n const allIncidents = [...groups.values()].sort((a, b) => b.decisions.length - a.decisions.length);\n const incidents = allIncidents\n .slice(0, limit)\n .map(group => ({ ...group, shown: group.decisions.slice(0, limit) }));\n // Check exactly the decisions this call returns, so every canRetry is real.\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n deps.boards,\n project,\n incidents.flatMap(group => group.shown),\n );\n const proposals = decisions.filter(decision => decision.status === 'proposed');\n const held = items.filter(isHeld);\n return {\n incidents: incidents.map(group => ({\n failureCode: group.failureCode,\n failureLabel: factoryDispatchFailureMetadata(group.decisions[0]!.failureCode).label,\n error: group.error,\n count: group.decisions.length,\n decisions: group.shown.map(decision => ({\n id: decision.id,\n ...ref(decision.workItemId),\n attempts: decision.attempts,\n canRetry: decisionRetryable(decision, overtaken.has(decision.id)),\n })),\n })),\n proposals: proposals\n .slice(0, limit)\n .map(decision => ({ ...summarizeDecision(decision), ...ref(decision.workItemId) })),\n held: held.slice(0, limit).map(item => ({\n ...ref(item.id),\n triageType: item.triageType,\n stage: workItemStage(item),\n })),\n truncated:\n allIncidents.length > limit ||\n incidents.some(group => group.shown.length < group.decisions.length) ||\n proposals.length > limit ||\n held.length > limit,\n };\n },\n }),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"read-tools.js","names":[],"sources":["../../src/tools/read-tools.ts"],"sourcesContent":["import { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { factorySessionUrl, factoryWorkItemUrl } from '../links.js';\nimport { decisionRetryable, overtakenDecisionIds } from '../rules/decision-applicability.js';\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type { AuditStorage } from '../storage/domains/audit/base.js';\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { sessionActivity } from './deliver.js';\nimport type { FactorySessionActivity } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport const MAX_TEXT = 600;\nexport const MAX_ERROR = 400;\nexport const MAX_LIST = 50;\nconst DEFAULT_LIST = 25;\n\nexport interface FactoryReadToolDependencies {\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'listDeferredDecisions'\n | 'listByIds'\n | 'listDecisionsForEvaluations'\n | 'listWorkItemSubscribers'\n | 'listSessionSubscriptions'\n >;\n comments: Pick<WorkItemCommentsStorage, 'listRecent'>;\n audit: Pick<AuditStorage, 'list'>;\n boards: BoardRegistry;\n /** The live controller, for whether a session is mid-run and when it last moved; absent before the mount is up. */\n messaging?: () => Promise<Pick<FactoryMessaging, 'controller'> | null>;\n}\n\nexport function truncate(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nfunction iso(value: Date | string | null | undefined): string | null {\n if (!value) return null;\n const date = value instanceof Date ? value : new Date(value);\n return Number.isFinite(date.getTime()) ? date.toISOString() : null;\n}\n\n/** The number a person sees on the card: the issue or PR number the item was filed from. */\nexport function workItemNumber(item: Pick<WorkItemRow, 'metadata'>): number | null {\n const number = item.metadata?.number ?? item.metadata?.githubIssueNumber ?? item.metadata?.githubPullRequestNumber;\n return typeof number === 'number' ? number : null;\n}\n\nexport function workItemStage(item: Pick<WorkItemRow, 'stages'>): string {\n return item.stages.join('+');\n}\n\n/** A work item id or its card number; anything else can't name an item. */\nexport const workItemRefSchema = z\n .string()\n .trim()\n .max(64)\n .refine(ref => z.string().uuid().safeParse(ref).success || /^#?\\d+$/.test(ref), {\n message: 'Use a work item id (UUID) or its number like \"#123\".',\n })\n .describe('Work item id, or its number like \"#123\".');\n\n/** Resolve `<uuid>` or `#123` to an item in this Factory only. Pass a ref validated by `workItemRefSchema`. */\nexport async function findWorkItem(\n workItems: Pick<WorkItemsStorage, 'list' | 'getForProject'>,\n scope: Pick<FactoryToolScope, 'orgId' | 'factoryProjectId'>,\n ref: string,\n): Promise<WorkItemRow | null> {\n const trimmed = ref.trim();\n const number = /^#?(\\d+)$/.exec(trimmed)?.[1];\n if (number === undefined) return workItems.getForProject(scope.orgId, scope.factoryProjectId, trimmed);\n const items = await workItems.list(scope);\n const matches = items.filter(item => workItemNumber(item) === Number(number));\n // Issue and PR numbers share a space on GitHub; prefer the Work-board card.\n return matches.find(item => item.externalSource?.type !== 'pull-request') ?? matches[0] ?? null;\n}\n\nexport function workItemNotFound(ref: string): Error {\n return new Error(`No work item ${ref} was found in this Factory.`);\n}\n\nfunction activeWorker(bindings: FactoryRunBindingRecord[], itemId: string) {\n const binding = bindings.find(candidate => candidate.workItemId === itemId && candidate.status === 'active');\n return binding ? { role: binding.role, sessionId: binding.sessionId } : null;\n}\n\n/** The card on its Factory board; null when the server has no public URL configured. */\nfunction workItemUrl(item: WorkItemRow) {\n return (\n factoryWorkItemUrl({\n factoryProjectId: item.factoryProjectId,\n board: boardForWorkItem(item),\n workItemId: item.id,\n }) ?? null\n );\n}\n\nfunction summarizeItem(item: WorkItemRow, bindings: FactoryRunBindingRecord[]) {\n return {\n id: item.id,\n number: workItemNumber(item),\n title: truncate(item.title, MAX_TEXT),\n board: boardForWorkItem(item),\n stage: workItemStage(item),\n url: workItemUrl(item),\n activeWorker: activeWorker(bindings, item.id),\n requestedBy: item.createdBy,\n updatedAt: iso(item.updatedAt),\n };\n}\n\nfunction summarizeDecision(decision: FactoryDeferredDecisionRecord, overtaken = false) {\n const type = typeof decision.decision.type === 'string' ? decision.decision.type : 'decision';\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : null;\n return {\n id: decision.id,\n workItemId: decision.workItemId,\n type,\n role,\n status: decision.status,\n attempts: decision.attempts,\n failureCode: decision.failureCode,\n failureLabel: decision.failureCode ? factoryDispatchFailureMetadata(decision.failureCode).label : null,\n canRetry: decisionRetryable(decision, overtaken),\n error: decision.lastError ? truncate(decision.lastError, MAX_ERROR) : null,\n approvedBy: decision.approvedBy,\n updatedAt: iso(decision.updatedAt),\n };\n}\n\n/** Held: a non-bug item triage parked until a person accepts it. */\nfunction isHeld(item: WorkItemRow): boolean {\n return item.stages.includes('triage') && !!item.triageType && item.triageType !== 'bug' && !item.acceptedAt;\n}\n\nconst listInputSchema = z\n .object({\n query: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Case-insensitive text in the title, or an exact card number like \"#123\".'),\n stage: z.string().min(1).max(64).optional().describe('Only items in this stage, e.g. \"triage\" or \"review\".'),\n board: z.string().min(1).max(64).optional().describe('Only items on this board, e.g. \"work\" or \"review\".'),\n role: z.string().min(1).max(64).optional().describe('Only items that have a session for this role.'),\n active: z.boolean().optional().describe('true: only items with an agent working now; false: only idle items.'),\n requestedBy: z.literal('me').optional().describe('\"me\": only items you filed.'),\n subscribed: z\n .boolean()\n .optional()\n .describe('true: only items this session follows; false: only items it does not.'),\n limit: z.number().int().min(1).max(MAX_LIST).optional().describe(`Default ${DEFAULT_LIST}, max ${MAX_LIST}.`),\n })\n .strict();\n\nexport function createFactoryReadTools(scope: FactoryToolScope, deps: FactoryReadToolDependencies): IntegrationTools {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n\n return {\n factory_list_work_items: createTool({\n id: 'factory_list_work_items',\n description:\n \"List this Factory's work items, newest activity first, with counts per stage for the whole Factory. Filter by text or #number, stage, board, role, whether an agent is working on it, items you filed, or items this session follows. Check here for an existing item before filing a new one.\",\n inputSchema: listInputSchema,\n execute: async input => {\n const [items, bindings, followed] = await Promise.all([\n deps.workItems.list(project),\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId),\n input.subscribed === undefined\n ? undefined\n : deps.workItems.listSessionSubscriptions({ ...project, sessionId: scope.sessionId }),\n ]);\n const followedIds = new Set(followed?.map(subscription => subscription.workItemId));\n const byStage: Record<string, number> = {};\n for (const item of items) {\n const stage = workItemStage(item);\n byStage[stage] = (byStage[stage] ?? 0) + 1;\n }\n const numberQuery = input.query ? /^#(\\d+)$/.exec(input.query)?.[1] : undefined;\n const text = input.query?.toLowerCase();\n const matches = items\n .filter(item => {\n if (numberQuery !== undefined) {\n if (workItemNumber(item) !== Number(numberQuery)) return false;\n } else if (text && !item.title.toLowerCase().includes(text)) return false;\n if (input.stage && !item.stages.includes(input.stage)) return false;\n if (input.board && boardForWorkItem(item) !== input.board) return false;\n if (input.role && !item.sessions[input.role]) return false;\n if (input.active !== undefined && (activeWorker(bindings, item.id) !== null) !== input.active) return false;\n if (input.requestedBy === 'me' && item.createdBy !== scope.userId) return false;\n if (input.subscribed !== undefined && followedIds.has(item.id) !== input.subscribed) return false;\n return true;\n })\n .sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime());\n const limit = input.limit ?? DEFAULT_LIST;\n return {\n counts: { total: items.length, byStage },\n items: matches.slice(0, limit).map(item => summarizeItem(item, bindings)),\n truncated: matches.length > limit,\n };\n },\n }),\n\n factory_inspect_work_item: createTool({\n id: 'factory_inspect_work_item',\n description:\n 'Everything the Factory knows about one work item: stage history, sessions by role (with whether each is mid-run and when its thread last moved), decisions with errors, recent audit events, recent feed comments, labels, linked items and the sessions following it. Use it to explain why an item is where it is.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const [bindings, decisions, audit, feed, all, subscribers] = await Promise.all([\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id),\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, { workItemId: item.id }),\n deps.audit.list({ ...project, limit: 200 }),\n deps.comments.listRecent({ ...project, workItemId: item.id, limit: 10 }),\n deps.workItems.list(project),\n deps.workItems.listWorkItemSubscribers({ orgId: scope.orgId, workItemId: item.id }),\n ]);\n const itemDecisions = decisions.slice(-MAX_LIST);\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n deps.boards,\n project,\n itemDecisions.filter(decision => decision.status === 'failed'),\n );\n const labels = Array.isArray(item.metadata?.labels)\n ? item.metadata.labels.filter((label): label is string => typeof label === 'string').slice(0, MAX_LIST)\n : [];\n const parent = item.parentWorkItemId\n ? all.find(candidate => candidate.id === item.parentWorkItemId)\n : undefined;\n const children = all.filter(candidate => candidate.parentWorkItemId === item.id);\n const controller = (await deps.messaging?.())?.controller;\n const activity = async (sessionId: string, threadId: string): Promise<FactorySessionActivity | null> =>\n controller ? sessionActivity(controller, sessionId, threadId) : null;\n const sessions = await Promise.all(\n Object.entries(item.sessions).map(async ([role, ref]) => ({\n role,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n url:\n factorySessionUrl({\n factoryProjectId: item.factoryProjectId,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n }) ?? null,\n startedBy: ref.startedBy,\n active: bindings.some(\n binding => binding.role === role && binding.sessionId === ref.sessionId && binding.status === 'active',\n ),\n // `running` with an old `lastActivityAt` is a stuck run; `running` with a recent one is a long one.\n ...((await activity(ref.sessionId, ref.threadId)) ?? { running: null, lastActivityAt: null }),\n })),\n );\n return {\n item: {\n ...summarizeItem(item, bindings),\n source: item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : 'manual',\n sourceUrl: typeof item.metadata?.url === 'string' ? item.metadata.url : null,\n triageType: item.triageType,\n revision: item.revision,\n },\n labels,\n stageHistory: item.stageHistory.slice(-MAX_LIST).map(entry => ({\n stage: entry.stage,\n enteredAt: entry.enteredAt,\n exitedAt: entry.exitedAt ?? null,\n by: entry.by,\n })),\n sessions,\n decisions: itemDecisions.map(decision => summarizeDecision(decision, overtaken.has(decision.id))),\n audit: audit.events\n .filter(event => event.targets.some(target => target.type === 'work_item' && target.id === item.id))\n .slice(0, 25)\n .map(event => ({\n action: event.action,\n actorId: event.actorId,\n actorType: event.actorType,\n occurredAt: iso(event.occurredAt),\n })),\n feed: feed.map(comment => ({\n id: comment.id,\n kind: comment.kind,\n author: comment.author,\n occurredAt: iso(comment.occurredAt),\n body: truncate(comment.body, MAX_TEXT),\n })),\n parent: parent ? summarizeItem(parent, bindings) : null,\n children: children.slice(0, MAX_LIST).map(child => summarizeItem(child, bindings)),\n subscribers: subscribers.slice(0, MAX_LIST).map(subscription => ({\n sessionId: subscription.sessionId,\n reason: subscription.reason,\n isCaller: subscription.sessionId === scope.sessionId,\n })),\n };\n },\n }),\n\n factory_list_attention: createTool({\n id: 'factory_list_attention',\n description:\n 'What in this Factory needs a person: failed decisions grouped by failure code and error (one incident per group), proposals waiting for approval, and items triage is holding until someone accepts them.',\n inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_LIST).optional() }).strict(),\n execute: async ({ limit = DEFAULT_LIST }) => {\n const [decisions, items] = await Promise.all([\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, {\n statuses: ['failed', 'proposed'],\n }),\n deps.workItems.list(project),\n ]);\n const itemsById = new Map(items.map(item => [item.id, item]));\n const failed = decisions.filter(decision => decision.status === 'failed');\n const groups = new Map<\n string,\n { failureCode: string; error: string; decisions: FactoryDeferredDecisionRecord[] }\n >();\n for (const decision of failed) {\n const error = truncate(decision.lastError ?? '', MAX_ERROR);\n const key = `${decision.failureCode ?? 'unknown'}|${error}`;\n const group = groups.get(key) ?? { failureCode: decision.failureCode ?? 'unknown', error, decisions: [] };\n group.decisions.push(decision);\n groups.set(key, group);\n }\n const ref = (workItemId: string | null) => {\n const item = workItemId ? itemsById.get(workItemId) : undefined;\n return {\n workItemId,\n number: item ? workItemNumber(item) : null,\n title: item ? truncate(item.title, MAX_TEXT) : null,\n url: item ? workItemUrl(item) : null,\n };\n };\n const allIncidents = [...groups.values()].sort((a, b) => b.decisions.length - a.decisions.length);\n const incidents = allIncidents\n .slice(0, limit)\n .map(group => ({ ...group, shown: group.decisions.slice(0, limit) }));\n // Check exactly the decisions this call returns, so every canRetry is real.\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n deps.boards,\n project,\n incidents.flatMap(group => group.shown),\n );\n const proposals = decisions.filter(decision => decision.status === 'proposed');\n const held = items.filter(isHeld);\n return {\n incidents: incidents.map(group => ({\n failureCode: group.failureCode,\n failureLabel: factoryDispatchFailureMetadata(group.decisions[0]!.failureCode).label,\n error: group.error,\n count: group.decisions.length,\n decisions: group.shown.map(decision => ({\n id: decision.id,\n ...ref(decision.workItemId),\n attempts: decision.attempts,\n canRetry: decisionRetryable(decision, overtaken.has(decision.id)),\n })),\n })),\n proposals: proposals\n .slice(0, limit)\n .map(decision => ({ ...summarizeDecision(decision), ...ref(decision.workItemId) })),\n held: held.slice(0, limit).map(item => ({\n ...ref(item.id),\n triageType: item.triageType,\n stage: workItemStage(item),\n })),\n truncated:\n allIncidents.length > limit ||\n incidents.some(group => group.shown.length < group.decisions.length) ||\n proposals.length > limit ||\n held.length > limit,\n };\n },\n }),\n };\n}\n"],"mappings":"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"}
@@ -76,7 +76,7 @@ async function resolveFactoryToolScope({ requestContext, authEnabled, sessions,
76
76
  */
77
77
  function requirePerson(scope, action) {
78
78
  if (scope.interactive) return;
79
- throw new Error(`Only a person can ${action}. Nobody signed in started this turn (it came from a notification, webhook or rule); ask the person to do it from the board or by messaging this session.`);
79
+ throw new Error(`Only a person can ${action}. Nobody signed in started this turn (it came from a notification, webhook or rule); ask the person to reply in their channel or message this session, and link the item so they can find it.`);
80
80
  }
81
81
  export { FACTORY_INTERACTIVE_KEY, FACTORY_SLACK_SENDER_KEY, requirePerson, resolveFactoryToolScope };
82
82
 
@@ -1 +1 @@
1
- {"version":3,"file":"scope.js","names":[],"sources":["../../src/tools/scope.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nimport {\n FACTORY_INTERACTIVE_KEY,\n factoryUserOrgId,\n getFactoryAuthUserFromContext,\n getFactoryAuthUserId,\n} from '../auth.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { isWorkerSession } from '../session/session-threads.js';\nimport { auditActorProfile } from '../storage/domains/audit/base.js';\nimport type { AuditActorProfileInput } from '../storage/domains/audit/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\nexport { FACTORY_INTERACTIVE_KEY };\n\n/** Linked Slack sender, separate from the session owner's model-credential identity. */\nexport const FACTORY_SLACK_SENDER_KEY = 'mastra__factorySlackSender';\n\n/** Who is calling Factory tools, and from which session. Everything a tool needs to stay in scope. */\nexport interface FactoryToolScope {\n orgId: string;\n factoryProjectId: string;\n /** The signed-in caller for a user session; the session row's owner for a worker. */\n userId: string;\n /** The source-control session id. */\n sessionId: string;\n /** The memory resource the session runs under. */\n resourceId: string;\n threadId: string;\n /** `worker` sessions are bound to a work item run; only `user` sessions manage the Factory. */\n kind: 'user' | 'worker';\n /** True only for turns a signed-in person started over HTTP (see {@link FACTORY_INTERACTIVE_KEY}). */\n interactive: boolean;\n /** The caller's name and avatar, stamped on audit events so the board can show who acted. */\n actorProfile?: AuditActorProfileInput;\n /** Set for a side session: the user session and thread that started it. */\n origin?: { sessionId: string; threadId: string };\n}\n\nexport interface ResolveFactoryToolScopeOptions {\n requestContext: RequestContext;\n /** `routeAuth.enabled()`; when false every caller is the shared `local` tenant. */\n authEnabled: boolean;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n projects: Pick<FactoryProjectsStorage, 'get'>;\n workItems: Pick<WorkItemsStorage, 'hasRunBindingForSession'>;\n}\n\n/**\n * Resolve the Factory a session's agent may act in. The scope comes from the\n * stored source-control session row and the authenticated caller, never from\n * the `factoryProjectId` thread tag alone: the browser sets that tag and\n * nothing clears it.\n */\nexport async function resolveFactoryToolScope({\n requestContext,\n authEnabled,\n sessions,\n projects,\n workItems,\n}: ResolveFactoryToolScopeOptions): Promise<FactoryToolScope | null> {\n if (typeof requestContext?.get !== 'function') return null;\n const controller = requestContext.get('controller') as AgentControllerRequestContext<unknown> | undefined;\n const sessionId = controller?.sessionId;\n const resourceId = controller?.resourceId;\n const threadId = controller?.threadId;\n if (!sessionId || !resourceId || !threadId) return null;\n\n let caller: { orgId: string; userId: string; actorProfile?: AuditActorProfileInput } | null = null;\n if (authEnabled) {\n const slackSender = requestContext.get(FACTORY_SLACK_SENDER_KEY) as { userId: string; orgId: string } | undefined;\n const user = getFactoryAuthUserFromContext(requestContext);\n if (slackSender && factoryUserOrgId(user) !== slackSender.orgId) return null;\n const userId = slackSender?.userId ?? getFactoryAuthUserId(user);\n const orgId = slackSender?.orgId ?? factoryUserOrgId(user);\n const actorProfile = slackSender ? undefined : auditActorProfile(user);\n if (userId && orgId) caller = { orgId, userId, ...(actorProfile ? { actorProfile } : {}) };\n } else {\n caller = { orgId: 'local', userId: 'local' };\n }\n\n const row = await sessions.getBySessionId(sessionId);\n if (!row?.factoryProjectId) return null;\n // A caller from another org never acts here, worker or not.\n if (caller && row.orgId !== caller.orgId) return null;\n\n const origin =\n row.originSessionId && row.originThreadId\n ? { sessionId: row.originSessionId, threadId: row.originThreadId }\n : undefined;\n const worker = await isWorkerSession(row, workItems);\n\n // A worker acts as its session row: dispatcher-driven runs carry no signed-in user.\n // A user session acts as the signed-in caller, who must be able to see it.\n let identity: { orgId: string; userId: string; actorProfile?: AuditActorProfileInput };\n if (worker) {\n identity = { orgId: row.orgId, userId: row.userId };\n } else {\n if (!caller) return null;\n if (row.visibility === 'private' && row.userId !== caller.userId) return null;\n identity = caller;\n }\n\n const project = await projects.get({ orgId: identity.orgId, id: row.factoryProjectId });\n if (!project) return null;\n\n return {\n orgId: identity.orgId,\n factoryProjectId: row.factoryProjectId,\n userId: identity.userId,\n sessionId: row.sessionId,\n resourceId,\n threadId,\n kind: worker ? 'worker' : 'user',\n interactive: requestContext.get(FACTORY_INTERACTIVE_KEY) === true,\n ...(identity.actorProfile ? { actorProfile: identity.actorProfile } : {}),\n ...(origin ? { origin } : {}),\n };\n}\n\n/**\n * Refuses an action only a signed-in person may take. Turns nobody signed in\n * started (notification wakes, webhooks, dispatcher notices) carry text other\n * people wrote, so they can't approve, resolve proposals, or waive plan approval.\n */\nexport function requirePerson(scope: FactoryToolScope, action: string): void {\n if (scope.interactive) return;\n throw new Error(\n `Only a person can ${action}. Nobody signed in started this turn (it came from a notification, webhook or rule); ask the person to do it from the board or by messaging this session.`,\n );\n}\n"],"mappings":";;;;;AAmBA,MAAa,2BAA2B;;;;;;;AAsCxC,eAAsB,wBAAwB,EAC5C,gBACA,aACA,UACA,UACA,aACmE;CACnE,IAAI,OAAO,gBAAgB,QAAQ,YAAY,OAAO;CACtD,MAAM,aAAa,eAAe,IAAI,YAAY;CAClD,MAAM,YAAY,YAAY;CAC9B,MAAM,aAAa,YAAY;CAC/B,MAAM,WAAW,YAAY;CAC7B,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,UAAU,OAAO;CAEnD,IAAI,SAA0F;CAC9F,IAAI,aAAa;EACf,MAAM,cAAc,eAAe,IAAI,wBAAwB;EAC/D,MAAM,OAAO,8BAA8B,cAAc;EACzD,IAAI,eAAe,iBAAiB,IAAI,MAAM,YAAY,OAAO,OAAO;EACxE,MAAM,SAAS,aAAa,UAAU,qBAAqB,IAAI;EAC/D,MAAM,QAAQ,aAAa,SAAS,iBAAiB,IAAI;EACzD,MAAM,eAAe,cAAc,KAAA,IAAY,kBAAkB,IAAI;EACrE,IAAI,UAAU,OAAO,SAAS;GAAE;GAAO;GAAQ,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;EAAG;CAC3F,OACE,SAAS;EAAE,OAAO;EAAS,QAAQ;CAAQ;CAG7C,MAAM,MAAM,MAAM,SAAS,eAAe,SAAS;CACnD,IAAI,CAAC,KAAK,kBAAkB,OAAO;CAEnC,IAAI,UAAU,IAAI,UAAU,OAAO,OAAO,OAAO;CAEjD,MAAM,SACJ,IAAI,mBAAmB,IAAI,iBACvB;EAAE,WAAW,IAAI;EAAiB,UAAU,IAAI;CAAe,IAC/D,KAAA;CACN,MAAM,SAAS,MAAM,gBAAgB,KAAK,SAAS;CAInD,IAAI;CACJ,IAAI,QACF,WAAW;EAAE,OAAO,IAAI;EAAO,QAAQ,IAAI;CAAO;MAC7C;EACL,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,IAAI,eAAe,aAAa,IAAI,WAAW,OAAO,QAAQ,OAAO;EACzE,WAAW;CACb;CAGA,IAAI,CAAC,MADiB,SAAS,IAAI;EAAE,OAAO,SAAS;EAAO,IAAI,IAAI;CAAiB,CAAC,GACxE,OAAO;CAErB,OAAO;EACL,OAAO,SAAS;EAChB,kBAAkB,IAAI;EACtB,QAAQ,SAAS;EACjB,WAAW,IAAI;EACf;EACA;EACA,MAAM,SAAS,WAAW;EAC1B,aAAa,eAAe,IAAI,uBAAuB,MAAM;EAC7D,GAAI,SAAS,eAAe,EAAE,cAAc,SAAS,aAAa,IAAI,CAAC;EACvE,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC7B;AACF;;;;;;AAOA,SAAgB,cAAc,OAAyB,QAAsB;CAC3E,IAAI,MAAM,aAAa;CACvB,MAAM,IAAI,MACR,qBAAqB,OAAO,0JAC9B;AACF"}
1
+ {"version":3,"file":"scope.js","names":[],"sources":["../../src/tools/scope.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nimport {\n FACTORY_INTERACTIVE_KEY,\n factoryUserOrgId,\n getFactoryAuthUserFromContext,\n getFactoryAuthUserId,\n} from '../auth.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { isWorkerSession } from '../session/session-threads.js';\nimport { auditActorProfile } from '../storage/domains/audit/base.js';\nimport type { AuditActorProfileInput } from '../storage/domains/audit/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\nexport { FACTORY_INTERACTIVE_KEY };\n\n/** Linked Slack sender, separate from the session owner's model-credential identity. */\nexport const FACTORY_SLACK_SENDER_KEY = 'mastra__factorySlackSender';\n\n/** Who is calling Factory tools, and from which session. Everything a tool needs to stay in scope. */\nexport interface FactoryToolScope {\n orgId: string;\n factoryProjectId: string;\n /** The signed-in caller for a user session; the session row's owner for a worker. */\n userId: string;\n /** The source-control session id. */\n sessionId: string;\n /** The memory resource the session runs under. */\n resourceId: string;\n threadId: string;\n /** `worker` sessions are bound to a work item run; only `user` sessions manage the Factory. */\n kind: 'user' | 'worker';\n /** True only for turns a signed-in person started over HTTP (see {@link FACTORY_INTERACTIVE_KEY}). */\n interactive: boolean;\n /** The caller's name and avatar, stamped on audit events so the board can show who acted. */\n actorProfile?: AuditActorProfileInput;\n /** Set for a side session: the user session and thread that started it. */\n origin?: { sessionId: string; threadId: string };\n}\n\nexport interface ResolveFactoryToolScopeOptions {\n requestContext: RequestContext;\n /** `routeAuth.enabled()`; when false every caller is the shared `local` tenant. */\n authEnabled: boolean;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n projects: Pick<FactoryProjectsStorage, 'get'>;\n workItems: Pick<WorkItemsStorage, 'hasRunBindingForSession'>;\n}\n\n/**\n * Resolve the Factory a session's agent may act in. The scope comes from the\n * stored source-control session row and the authenticated caller, never from\n * the `factoryProjectId` thread tag alone: the browser sets that tag and\n * nothing clears it.\n */\nexport async function resolveFactoryToolScope({\n requestContext,\n authEnabled,\n sessions,\n projects,\n workItems,\n}: ResolveFactoryToolScopeOptions): Promise<FactoryToolScope | null> {\n if (typeof requestContext?.get !== 'function') return null;\n const controller = requestContext.get('controller') as AgentControllerRequestContext<unknown> | undefined;\n const sessionId = controller?.sessionId;\n const resourceId = controller?.resourceId;\n const threadId = controller?.threadId;\n if (!sessionId || !resourceId || !threadId) return null;\n\n let caller: { orgId: string; userId: string; actorProfile?: AuditActorProfileInput } | null = null;\n if (authEnabled) {\n const slackSender = requestContext.get(FACTORY_SLACK_SENDER_KEY) as { userId: string; orgId: string } | undefined;\n const user = getFactoryAuthUserFromContext(requestContext);\n if (slackSender && factoryUserOrgId(user) !== slackSender.orgId) return null;\n const userId = slackSender?.userId ?? getFactoryAuthUserId(user);\n const orgId = slackSender?.orgId ?? factoryUserOrgId(user);\n const actorProfile = slackSender ? undefined : auditActorProfile(user);\n if (userId && orgId) caller = { orgId, userId, ...(actorProfile ? { actorProfile } : {}) };\n } else {\n caller = { orgId: 'local', userId: 'local' };\n }\n\n const row = await sessions.getBySessionId(sessionId);\n if (!row?.factoryProjectId) return null;\n // A caller from another org never acts here, worker or not.\n if (caller && row.orgId !== caller.orgId) return null;\n\n const origin =\n row.originSessionId && row.originThreadId\n ? { sessionId: row.originSessionId, threadId: row.originThreadId }\n : undefined;\n const worker = await isWorkerSession(row, workItems);\n\n // A worker acts as its session row: dispatcher-driven runs carry no signed-in user.\n // A user session acts as the signed-in caller, who must be able to see it.\n let identity: { orgId: string; userId: string; actorProfile?: AuditActorProfileInput };\n if (worker) {\n identity = { orgId: row.orgId, userId: row.userId };\n } else {\n if (!caller) return null;\n if (row.visibility === 'private' && row.userId !== caller.userId) return null;\n identity = caller;\n }\n\n const project = await projects.get({ orgId: identity.orgId, id: row.factoryProjectId });\n if (!project) return null;\n\n return {\n orgId: identity.orgId,\n factoryProjectId: row.factoryProjectId,\n userId: identity.userId,\n sessionId: row.sessionId,\n resourceId,\n threadId,\n kind: worker ? 'worker' : 'user',\n interactive: requestContext.get(FACTORY_INTERACTIVE_KEY) === true,\n ...(identity.actorProfile ? { actorProfile: identity.actorProfile } : {}),\n ...(origin ? { origin } : {}),\n };\n}\n\n/**\n * Refuses an action only a signed-in person may take. Turns nobody signed in\n * started (notification wakes, webhooks, dispatcher notices) carry text other\n * people wrote, so they can't approve, resolve proposals, or waive plan approval.\n */\nexport function requirePerson(scope: FactoryToolScope, action: string): void {\n if (scope.interactive) return;\n throw new Error(\n `Only a person can ${action}. Nobody signed in started this turn (it came from a notification, webhook or rule); ask the person to reply in their channel or message this session, and link the item so they can find it.`,\n );\n}\n"],"mappings":";;;;;AAmBA,MAAa,2BAA2B;;;;;;;AAsCxC,eAAsB,wBAAwB,EAC5C,gBACA,aACA,UACA,UACA,aACmE;CACnE,IAAI,OAAO,gBAAgB,QAAQ,YAAY,OAAO;CACtD,MAAM,aAAa,eAAe,IAAI,YAAY;CAClD,MAAM,YAAY,YAAY;CAC9B,MAAM,aAAa,YAAY;CAC/B,MAAM,WAAW,YAAY;CAC7B,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,UAAU,OAAO;CAEnD,IAAI,SAA0F;CAC9F,IAAI,aAAa;EACf,MAAM,cAAc,eAAe,IAAI,wBAAwB;EAC/D,MAAM,OAAO,8BAA8B,cAAc;EACzD,IAAI,eAAe,iBAAiB,IAAI,MAAM,YAAY,OAAO,OAAO;EACxE,MAAM,SAAS,aAAa,UAAU,qBAAqB,IAAI;EAC/D,MAAM,QAAQ,aAAa,SAAS,iBAAiB,IAAI;EACzD,MAAM,eAAe,cAAc,KAAA,IAAY,kBAAkB,IAAI;EACrE,IAAI,UAAU,OAAO,SAAS;GAAE;GAAO;GAAQ,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;EAAG;CAC3F,OACE,SAAS;EAAE,OAAO;EAAS,QAAQ;CAAQ;CAG7C,MAAM,MAAM,MAAM,SAAS,eAAe,SAAS;CACnD,IAAI,CAAC,KAAK,kBAAkB,OAAO;CAEnC,IAAI,UAAU,IAAI,UAAU,OAAO,OAAO,OAAO;CAEjD,MAAM,SACJ,IAAI,mBAAmB,IAAI,iBACvB;EAAE,WAAW,IAAI;EAAiB,UAAU,IAAI;CAAe,IAC/D,KAAA;CACN,MAAM,SAAS,MAAM,gBAAgB,KAAK,SAAS;CAInD,IAAI;CACJ,IAAI,QACF,WAAW;EAAE,OAAO,IAAI;EAAO,QAAQ,IAAI;CAAO;MAC7C;EACL,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,IAAI,eAAe,aAAa,IAAI,WAAW,OAAO,QAAQ,OAAO;EACzE,WAAW;CACb;CAGA,IAAI,CAAC,MADiB,SAAS,IAAI;EAAE,OAAO,SAAS;EAAO,IAAI,IAAI;CAAiB,CAAC,GACxE,OAAO;CAErB,OAAO;EACL,OAAO,SAAS;EAChB,kBAAkB,IAAI;EACtB,QAAQ,SAAS;EACjB,WAAW,IAAI;EACf;EACA;EACA,MAAM,SAAS,WAAW;EAC1B,aAAa,eAAe,IAAI,uBAAuB,MAAM;EAC7D,GAAI,SAAS,eAAe,EAAE,cAAc,SAAS,aAAa,IAAI,CAAC;EACvE,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC7B;AACF;;;;;;AAOA,SAAgB,cAAc,OAAyB,QAAsB;CAC3E,IAAI,MAAM,aAAa;CACvB,MAAM,IAAI,MACR,qBAAqB,OAAO,8LAC9B;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAKhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAAoD,CAAC;AAExF,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,6BAA6B,CAAC,CAAC;IAC7F,YAAY,EAAE;QACZ,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,EAAE,oBAAoB,CAAC;QACpC;;;;WAIG;QACH,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,eAAe,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,MAAM,CAAC,CAAC;YACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,wEAAwE;IACxE,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,wBAAwB,GACxB,yBAAyB,GACzB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,yBAAyB,CAC5B,CAAC;IACF,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1C,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC9E;AA0BD,oGAAoG;AACpG,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,8BAA8B,GACnC,gBAAgB,CA+alB"}
1
+ {"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAMhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAAoD,CAAC;AAExF,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,6BAA6B,CAAC,CAAC;IAC7F,YAAY,EAAE;QACZ,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,EAAE,oBAAoB,CAAC;QACpC;;;;WAIG;QACH,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,eAAe,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,MAAM,CAAC,CAAC;YACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,wEAAwE;IACxE,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,wBAAwB,GACxB,yBAAyB,GACzB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,yBAAyB,CAC5B,CAAC;IACF,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1C,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC9E;AA0BD,oGAAoG;AACpG,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,8BAA8B,GACnC,gBAAgB,CA0clB"}
@@ -4,6 +4,7 @@ import { primeTenantCredentialsForTenant } from "../routes/tenant-credentials.js
4
4
  import { effectiveModelScope } from "../storage/domains/projects/base.js";
5
5
  import { ensureFactorySourceSession, hydrateFactorySession, resolveFactorySessionModel } from "../session/factory-session.js";
6
6
  import { resolveSessionThread } from "./deliver.js";
7
+ import { factorySessionUrl, factoryWorkItemUrl } from "../links.js";
7
8
  import { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from "./read-tools.js";
8
9
  import { sessionModel } from "./side-sessions.js";
9
10
  import { resolveFactoryModelPolicy } from "../session/credential-policy.js";
@@ -139,7 +140,7 @@ function createFactorySessionTools(scope, deps) {
139
140
  "A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.",
140
141
  "Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.",
141
142
  "With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.",
142
- "A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to use the board."
143
+ "A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to start it from a signed-in chat."
143
144
  ].join(" "),
144
145
  inputSchema: z.object({
145
146
  prompt: z.string().trim().min(1).max(8e3).describe("The first message the new session receives."),
@@ -153,7 +154,7 @@ function createFactorySessionTools(scope, deps) {
153
154
  }),
154
155
  execute: async ({ prompt, title, model, workItem, role }) => {
155
156
  if (workItem !== void 0) {
156
- if (!scope.interactive) throw new Error("Starting a role on a work item needs a person in this turn; start a side session, or ask the person to use the board.");
157
+ if (!scope.interactive) throw new Error("Starting a role on a work item needs a person in this turn; start a side session, or ask the person to ask for it in a signed-in chat.");
157
158
  return startWorkItemRole({
158
159
  prompt,
159
160
  title,
@@ -255,7 +256,12 @@ function createFactorySessionTools(scope, deps) {
255
256
  mode: "side",
256
257
  sessionId: ensured.sessionId,
257
258
  threadId,
258
- title: threadTitle
259
+ title: threadTitle,
260
+ url: factorySessionUrl({
261
+ factoryProjectId: scope.factoryProjectId,
262
+ sessionId: ensured.sessionId,
263
+ threadId
264
+ }) ?? null
259
265
  };
260
266
  }
261
267
  });
@@ -346,7 +352,7 @@ function createFactorySessionTools(scope, deps) {
346
352
  }
347
353
  const replaced = (await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id)).find((binding) => binding.role === role && binding.id !== prepared.bindingId && binding.revokedAt !== null && binding.revokedAt >= startedAt);
348
354
  const session = await messaging.controller.getSession(prepared.sessionId);
349
- if (!session) throw new Error(`The ${role} session was bound but is not running; open it from the board.`);
355
+ if (!session) throw new Error(`The ${role} session ${prepared.sessionId} was bound but is not running; message it with factory_signal_session to wake it.`);
350
356
  const kickoffId = globalThis.crypto.randomUUID();
351
357
  const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);
352
358
  const kickoffContext = new RequestContext();
@@ -393,11 +399,21 @@ function createFactorySessionTools(scope, deps) {
393
399
  mode: "work-item",
394
400
  workItem: {
395
401
  id: item.id,
396
- ...number === null ? {} : { number }
402
+ ...number === null ? {} : { number },
403
+ url: factoryWorkItemUrl({
404
+ factoryProjectId: scope.factoryProjectId,
405
+ board: boardForWorkItem(item),
406
+ workItemId: item.id
407
+ }) ?? null
397
408
  },
398
409
  role,
399
410
  sessionId: prepared.sessionId,
400
411
  threadId: prepared.threadId,
412
+ url: factorySessionUrl({
413
+ factoryProjectId: scope.factoryProjectId,
414
+ sessionId: prepared.sessionId,
415
+ threadId: prepared.threadId
416
+ }) ?? null,
401
417
  bindingId: prepared.bindingId,
402
418
  ...replaced ? { replacedSessionId: replaced.sessionId } : {}
403
419
  };
@@ -1 +1 @@
1
- {"version":3,"file":"session-tools.js","names":[],"sources":["../../src/tools/session-tools.ts"],"sourcesContent":["import { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { buildModelCatalog, listSessionScopedCredentials } from '../routes/config.js';\nimport { USER_SESSION_BRANCH_PREFIX } from '../routes/source-control-sessions.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { FactoryStartTransitionError, WorkItemRepositoryError } from '../rules/start-coordinator.js';\nimport type { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { resolveFactoryModelPolicy } from '../session/credential-policy.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport {\n ensureFactorySourceSession,\n hydrateFactorySession,\n resolveFactorySessionModel,\n} from '../session/factory-session.js';\nimport { recordSessionRunStart } from '../session/run-audit.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { deliverFactorySessionNotification, resolveSessionThread } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from './read-tools.js';\nimport { heartbeatOpenRunUntilEnd } from './run-watch.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { sessionModel } from './side-sessions.js';\n\nconst TITLE_LENGTH = 120;\n\n/** Who renews the open-run entries this process writes; one id per process, like the dispatcher's. */\nexport const FACTORY_TOOLS_OWNER_ID = `factory-tools:${globalThis.crypto.randomUUID()}`;\n\nexport interface FactorySessionToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'getSourceControlBySessionId'>;\n sessionModel: {\n projects: FactoryProjectsStorage;\n modelDefaults: ModelDefaultsStorage;\n /**\n * Tenant credential stores, present when auth is on. Model switches then\n * check the target session's credential scope, like the Settings picker;\n * without them the server's own keys decide.\n */\n tenantCatalog?: {\n credentials: Pick<ModelCredentialsStorage, 'listCredentials'>;\n customProviders?: Pick<CustomProvidersStorage, 'ensureReady' | 'list'>;\n deploymentProviders?: ReadonlySet<string>;\n };\n };\n /** Resolved lazily: the controller only exists once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'findRunBindingByThread'\n | 'hasRunBindingForSession'\n | 'subscribeToWorkItem'\n | 'unsubscribeFromWorkItem'\n | 'listSessionSubscriptions'\n | 'listWorkItemSubscribers'\n >;\n boards: BoardRegistry;\n auditRecorder: AuditRecorder;\n intake?: Pick<IntakeStorage, 'getConfig'>;\n /** The browser Start's coordinator; absent until the Factory runtime is ready. */\n startCoordinator: () => Pick<FactoryStartCoordinator, 'prepare'> | undefined;\n}\n\nconst LISTED_MODELS = 8;\n\nfunction ambiguousRepository(candidates: readonly string[]): string {\n return `This item has several repositories (${candidates.join(', ')}); v1 can't choose one.`;\n}\n\nfunction startErrorMessage(error: unknown): string | null {\n if (error instanceof WorkItemRepositoryError) {\n return error.code === 'repository_ambiguous' ? ambiguousRepository(error.candidates) : error.message;\n }\n if (error instanceof FactoryStartTransitionError) return `The start was refused: ${error.result.reason}`;\n return null;\n}\n\nfunction workItemLabel(item: WorkItemRow): string {\n const number = workItemNumber(item);\n return number === null ? `\"${item.title}\"` : `#${number}`;\n}\n\nfunction titleFrom(prompt: string): string {\n const firstLine = prompt.split('\\n').find(line => line.trim()) ?? prompt;\n return firstLine.trim().slice(0, TITLE_LENGTH);\n}\n\n/** Tools that start sessions on the caller's behalf, or switch their models. User sessions only. */\nexport function createFactorySessionTools(\n scope: FactoryToolScope,\n deps: FactorySessionToolDependencies,\n): IntegrationTools {\n /** The models a session can switch to, resolved from the target session's owner and project policy. */\n async function sessionModelCatalog(\n controller: FactoryMessaging['controller'],\n sessionId: string,\n threadId: string,\n resourceId: string,\n ): Promise<{ usable: string[]; known: Set<string>; scopeLabel: string }> {\n const models = await controller.listAvailableModels();\n const known = new Set(models.flatMap(model => (typeof model.id === 'string' ? [model.id] : [])));\n const tenantCatalog = deps.sessionModel.tenantCatalog;\n if (!tenantCatalog) {\n const usable = (await buildModelCatalog({ controller })).map(model => model.id);\n return { usable, known, scopeLabel: 'server' };\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { id: scope.userId, organizationId: scope.orgId });\n requestContext.set('controller', { sessionId, resourceId, threadId });\n const policy = await resolveFactoryModelPolicy(requestContext, {\n projects: deps.sessionModel.projects,\n sourceControl: deps.sessions,\n workItems: deps.workItems,\n });\n if (!policy) throw new Error('Session model policy is unavailable.');\n const modelScope = policy.tenant.credentialScope ?? 'user';\n const orgId = policy.tenant.orgId ?? scope.orgId;\n const customProviders = tenantCatalog.customProviders;\n const usable = (\n await buildModelCatalog({\n controller,\n tenantCredentials: await listSessionScopedCredentials({\n credentials: tenantCatalog.credentials,\n orgId,\n userId: policy.tenant.userId,\n modelScope,\n }),\n deploymentProviders: tenantCatalog.deploymentProviders,\n listCustomProviders: customProviders\n ? async () => {\n await customProviders.ensureReady();\n return customProviders.list({ orgId });\n }\n : undefined,\n })\n ).map(model => model.id);\n for (const id of usable) known.add(id);\n return { usable, known, scopeLabel: modelScope === 'factory' ? 'Factory' : 'session owner' };\n }\n\n function unrunnableModelError(\n modelId: string,\n catalog: { usable: string[]; known: Set<string>; scopeLabel: string },\n ) {\n const options = catalog.usable.length\n ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(', ')}${catalog.usable.length > LISTED_MODELS ? ', …' : ''}.`\n : ' No models are available to it; connect a provider credential first.';\n return new Error(\n catalog.known.has(modelId)\n ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}`\n : `Unknown model ${modelId}.${options}`,\n );\n }\n\n const switchModel = createTool({\n id: 'factory_switch_session_model',\n description:\n 'Look up or switch the model of a session you own in this Factory (side, work item, or review session). When someone describes a model by purpose, family, or preference rather than an exact ID, call with sessionId alone first to list models the target session can actually use. Interpret their intent against that list, then call with the chosen exact modelId to switch; do not use their words as a guessed model ID. A running session switches immediately; a stopped one uses the model on its next run. User-scoped sessions use their owner credentials; Factory-scoped sessions use Factory credentials. Use a session ID from the Factory session or work item tools.',\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200),\n modelId: z.string().trim().min(1).max(200).optional().describe('Omit to list usable models without switching.'),\n })\n .strict(),\n execute: async ({ sessionId, modelId }) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.userId !== scope.userId\n ) {\n throw new Error('Session not found or not owned by you in this Factory.');\n }\n // A turn nobody signed in started reaches only this session and the side sessions it started.\n if (!scope.interactive && sessionId !== scope.sessionId && row.originSessionId !== scope.sessionId) {\n throw new Error(\n 'Without a person in this turn, only this session and the side sessions it started can switch models.',\n );\n }\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session model switching is not available on this server.');\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('Session has no conversation thread yet.');\n // The thread's resource, not the session id: sessions share their Factory's or owner's resource.\n const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;\n const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);\n if (modelId === undefined) return { sessionId, models: catalog.usable };\n if (!catalog.usable.includes(modelId)) throw unrunnableModelError(modelId, catalog);\n const { live } = await messaging.controller.setThreadModel({ resourceId, threadId, modelId });\n return { sessionId, threadId, modelId, live };\n },\n });\n\n const start = createTool({\n id: 'factory_start_session',\n description: [\n 'Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.',\n 'A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.',\n 'Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.',\n 'With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.',\n \"A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to use the board.\",\n ].join(' '),\n inputSchema: z\n .object({\n prompt: z.string().trim().min(1).max(8000).describe('The first message the new session receives.'),\n title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),\n model: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe(\n 'Model id for a side session. Omit it to inherit this session’s model. The session credentials must be able to run it.',\n ),\n workItem: workItemRefSchema.optional(),\n role: z.string().trim().min(1).max(64).optional(),\n })\n .strict()\n .refine(input => input.role === undefined || input.workItem !== undefined, {\n message: 'Pass workItem together with role.',\n path: ['role'],\n }),\n execute: async ({ prompt, title, model, workItem, role }) => {\n if (workItem !== undefined) {\n // A role start records a run as the person, so it waits for a turn the person started.\n if (!scope.interactive) {\n throw new Error(\n 'Starting a role on a work item needs a person in this turn; start a side session, or ask the person to use the board.',\n );\n }\n return startWorkItemRole({ prompt, title, workItem, role });\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n\n // The child runs with the same credentials as this session, so validate against those.\n const parentThreadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId: scope.sessionId,\n });\n const parentResourceId = parentThreadId\n ? ((await messaging.controller.queryThreadById({ threadId: parentThreadId }))?.resourceId ?? scope.sessionId)\n : scope.sessionId;\n const catalog = await sessionModelCatalog(\n messaging.controller,\n scope.sessionId,\n parentThreadId ?? scope.threadId,\n parentResourceId,\n );\n const inherited = model\n ? null\n : await sessionModel(messaging.controller, deps.sessions, deps.workItems, scope.sessionId);\n const requestedModel = model ?? (inherited && catalog.usable.includes(inherited) ? inherited : undefined);\n if (requestedModel && !catalog.usable.includes(requestedModel))\n throw unrunnableModelError(requestedModel, catalog);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n // Bills the caller: the credential tenant follows the row's user.\n attributeToUserId: scope.userId,\n // Same repository as the session that started it.\n ...(parent.projectRepositoryId ? { projectRepositoryId: parent.projectRepositoryId } : {}),\n visibility: parent.visibility,\n origin: { sessionId: scope.sessionId, threadId: scope.threadId },\n });\n const resolvedModel = await resolveFactorySessionModel({\n ...deps.sessionModel,\n factoryProjectId: scope.factoryProjectId,\n orgId: scope.orgId,\n ownerUserId: scope.userId,\n // No work item, so an interactive session: a hybrid Factory bills it as one.\n context: 'session',\n });\n\n // Only the new row's owner as `user`. The tool call's context carries the\n // interactive marker and the parent's controller entry; neither may reach the child.\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n\n const tags = { factoryProjectId: scope.factoryProjectId, projectRepositoryId: ensured.projectRepositoryId };\n const session = await messaging.controller.createSession({\n id: ensured.sessionId,\n ownerId: scope.userId,\n // A side session works for the Factory, so it shares the Factory's memory\n // resource; a private one stays with its owner, like a Slack DM.\n resourceId: parent.visibility === 'private' ? scope.userId : scope.factoryProjectId,\n threadId: ensured.sessionId,\n requestContext: kickoffContext,\n tags,\n });\n await session.state.set(tags);\n await hydrateFactorySession(session, {\n orgId: scope.orgId,\n modelScope: resolvedModel.modelScope,\n ownerUserId: scope.userId,\n defaultModelId: requestedModel ?? resolvedModel.modelId,\n });\n\n const threadId = session.thread.requireId();\n const threadTitle = title ?? titleFrom(prompt);\n await session.thread.rename({ title: threadTitle, pin: false });\n // On the row too, so its run-end reports name it without waiting for the title mirror.\n await sourceControl.sessions.rename({ sessionId: ensured.sessionId, title: threadTitle });\n await session.thread.setSetting({ key: 'factorySessionId', value: ensured.sessionId });\n\n await primeTenantCredentialsForTenant({\n orgId: scope.orgId,\n userId: scope.userId,\n credentialScope: resolvedModel.modelScope,\n });\n // How its runs end reaches this session through the listener every Factory\n // session gets (`observeSessionRunReports`), which follows the row's origin.\n // `requireDelivery` makes `accepted` reject when the wake fails; the run itself is not awaited.\n await session.sendSignal(\n { id: globalThis.crypto.randomUUID(), type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n\n return { mode: 'side' as const, sessionId: ensured.sessionId, threadId, title: threadTitle };\n },\n });\n\n async function startWorkItemRole({\n prompt,\n title,\n workItem,\n role,\n }: {\n prompt: string;\n title?: string;\n workItem: string;\n role?: string;\n }) {\n if (!role) throw new Error('Pass the role to start on the work item.');\n const coordinator = deps.startCoordinator();\n if (!coordinator) throw new Error(\"Factory sessions can't be started yet; try again shortly.\");\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const label = workItemLabel(item);\n const board = deps.boards.get(boardForWorkItem(item));\n if (!board?.phaseForRole(role)) throw new Error(`${label}'s board has no ${role} role.`);\n const seated = item.sessions[role];\n if (seated) {\n throw new Error(\n `${label} already has a ${role} session ${seated.sessionId}; message it with factory_signal_session.`,\n );\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n const project = await deps.sessionModel.projects.get({ orgId: scope.orgId, id: scope.factoryProjectId });\n if (!project) throw new Error('This Factory no longer exists.');\n // A work-item start is a run, so a hybrid Factory uses the Factory's scope (as the Start route does).\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n const projectRepositoryId = await resolveRepository(sourceControl, item);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n projectRepositoryId,\n attributeToUserId: scope.userId,\n // A run works a shared card, so the org can open it and answer its approvals even when the session that\n // started it is private.\n visibility: 'org',\n });\n\n // Follow first, so a fast run's first report already has somewhere to go.\n const followScope = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const alreadyFollowing = (\n await deps.workItems.listSessionSubscriptions({ ...followScope, sessionId: scope.sessionId })\n ).some(subscription => subscription.workItemId === item.id);\n await deps.workItems.subscribeToWorkItem({\n ...followScope,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason: 'started',\n });\n\n const startedAt = new Date();\n let prepared;\n try {\n prepared = await coordinator.prepare({\n orgId: scope.orgId,\n userId: scope.userId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: ensured.sessionId,\n threadTitle: title ?? `${label} ${role}`,\n kickoffKey: globalThis.crypto.randomUUID(),\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n workItem: {\n id: item.id,\n role,\n input: {\n externalSource: item.externalSource,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n stages: item.stages,\n sessions: item.sessions,\n metadata: item.metadata,\n },\n },\n });\n } catch (error) {\n if (!alreadyFollowing) {\n await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n }\n const message = startErrorMessage(error);\n if (message) throw new Error(message);\n throw error;\n }\n\n const bindings = await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const replaced = bindings.find(\n binding =>\n binding.role === role &&\n binding.id !== prepared.bindingId &&\n binding.revokedAt !== null &&\n binding.revokedAt >= startedAt,\n );\n\n const session = await messaging.controller.getSession(prepared.sessionId);\n if (!session) throw new Error(`The ${role} session was bound but is not running; open it from the board.`);\n const kickoffId = globalThis.crypto.randomUUID();\n // Heartbeat from before the kickoff so a fast run's end is seen; the open-run\n // entry and audit follow the accepted kickoff, as the dispatcher records them.\n // The run's end reaches this item's followers through the session's listener.\n const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n try {\n await session.sendSignal(\n { id: kickoffId, type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n } catch (error) {\n heartbeat.stop();\n throw error;\n }\n\n await recordSessionRunStart(session, {\n audit: deps.auditRecorder,\n actorType: 'human',\n observedEnd: heartbeat.endReason,\n run: {\n kickoffId,\n ownerId: FACTORY_TOOLS_OWNER_ID,\n heartbeatAt: Date.now(),\n bindingId: prepared.bindingId,\n role,\n startedBy: scope.userId,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: item.id,\n workItemName: item.title,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n branch: prepared.branch,\n },\n });\n\n const number = workItemNumber(item);\n return {\n mode: 'work-item' as const,\n workItem: { id: item.id, ...(number === null ? {} : { number }) },\n role,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n bindingId: prepared.bindingId,\n ...(replaced ? { replacedSessionId: replaced.sessionId } : {}),\n };\n }\n\n async function resolveRepository(sourceControl: SourceControlStorageHandle, item: WorkItemRow): Promise<string> {\n const intakeConfig = await deps.intake?.getConfig({ orgId: scope.orgId, integrationIds: ['linear'] });\n const repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n throw new Error(ambiguousRepository(repository.candidates));\n }\n if (repository.status === 'unlinked') throw new Error(repository.hint);\n return repository.projectRepositoryId;\n }\n\n return { factory_start_session: start, factory_switch_session_model: switchModel };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-tools.js","names":[],"sources":["../../src/tools/session-tools.ts"],"sourcesContent":["import { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { factorySessionUrl, factoryWorkItemUrl } from '../links.js';\nimport { buildModelCatalog, listSessionScopedCredentials } from '../routes/config.js';\nimport { USER_SESSION_BRANCH_PREFIX } from '../routes/source-control-sessions.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { FactoryStartTransitionError, WorkItemRepositoryError } from '../rules/start-coordinator.js';\nimport type { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { resolveFactoryModelPolicy } from '../session/credential-policy.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport {\n ensureFactorySourceSession,\n hydrateFactorySession,\n resolveFactorySessionModel,\n} from '../session/factory-session.js';\nimport { recordSessionRunStart } from '../session/run-audit.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { deliverFactorySessionNotification, resolveSessionThread } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from './read-tools.js';\nimport { heartbeatOpenRunUntilEnd } from './run-watch.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { sessionModel } from './side-sessions.js';\n\nconst TITLE_LENGTH = 120;\n\n/** Who renews the open-run entries this process writes; one id per process, like the dispatcher's. */\nexport const FACTORY_TOOLS_OWNER_ID = `factory-tools:${globalThis.crypto.randomUUID()}`;\n\nexport interface FactorySessionToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'getSourceControlBySessionId'>;\n sessionModel: {\n projects: FactoryProjectsStorage;\n modelDefaults: ModelDefaultsStorage;\n /**\n * Tenant credential stores, present when auth is on. Model switches then\n * check the target session's credential scope, like the Settings picker;\n * without them the server's own keys decide.\n */\n tenantCatalog?: {\n credentials: Pick<ModelCredentialsStorage, 'listCredentials'>;\n customProviders?: Pick<CustomProvidersStorage, 'ensureReady' | 'list'>;\n deploymentProviders?: ReadonlySet<string>;\n };\n };\n /** Resolved lazily: the controller only exists once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'findRunBindingByThread'\n | 'hasRunBindingForSession'\n | 'subscribeToWorkItem'\n | 'unsubscribeFromWorkItem'\n | 'listSessionSubscriptions'\n | 'listWorkItemSubscribers'\n >;\n boards: BoardRegistry;\n auditRecorder: AuditRecorder;\n intake?: Pick<IntakeStorage, 'getConfig'>;\n /** The browser Start's coordinator; absent until the Factory runtime is ready. */\n startCoordinator: () => Pick<FactoryStartCoordinator, 'prepare'> | undefined;\n}\n\nconst LISTED_MODELS = 8;\n\nfunction ambiguousRepository(candidates: readonly string[]): string {\n return `This item has several repositories (${candidates.join(', ')}); v1 can't choose one.`;\n}\n\nfunction startErrorMessage(error: unknown): string | null {\n if (error instanceof WorkItemRepositoryError) {\n return error.code === 'repository_ambiguous' ? ambiguousRepository(error.candidates) : error.message;\n }\n if (error instanceof FactoryStartTransitionError) return `The start was refused: ${error.result.reason}`;\n return null;\n}\n\nfunction workItemLabel(item: WorkItemRow): string {\n const number = workItemNumber(item);\n return number === null ? `\"${item.title}\"` : `#${number}`;\n}\n\nfunction titleFrom(prompt: string): string {\n const firstLine = prompt.split('\\n').find(line => line.trim()) ?? prompt;\n return firstLine.trim().slice(0, TITLE_LENGTH);\n}\n\n/** Tools that start sessions on the caller's behalf, or switch their models. User sessions only. */\nexport function createFactorySessionTools(\n scope: FactoryToolScope,\n deps: FactorySessionToolDependencies,\n): IntegrationTools {\n /** The models a session can switch to, resolved from the target session's owner and project policy. */\n async function sessionModelCatalog(\n controller: FactoryMessaging['controller'],\n sessionId: string,\n threadId: string,\n resourceId: string,\n ): Promise<{ usable: string[]; known: Set<string>; scopeLabel: string }> {\n const models = await controller.listAvailableModels();\n const known = new Set(models.flatMap(model => (typeof model.id === 'string' ? [model.id] : [])));\n const tenantCatalog = deps.sessionModel.tenantCatalog;\n if (!tenantCatalog) {\n const usable = (await buildModelCatalog({ controller })).map(model => model.id);\n return { usable, known, scopeLabel: 'server' };\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { id: scope.userId, organizationId: scope.orgId });\n requestContext.set('controller', { sessionId, resourceId, threadId });\n const policy = await resolveFactoryModelPolicy(requestContext, {\n projects: deps.sessionModel.projects,\n sourceControl: deps.sessions,\n workItems: deps.workItems,\n });\n if (!policy) throw new Error('Session model policy is unavailable.');\n const modelScope = policy.tenant.credentialScope ?? 'user';\n const orgId = policy.tenant.orgId ?? scope.orgId;\n const customProviders = tenantCatalog.customProviders;\n const usable = (\n await buildModelCatalog({\n controller,\n tenantCredentials: await listSessionScopedCredentials({\n credentials: tenantCatalog.credentials,\n orgId,\n userId: policy.tenant.userId,\n modelScope,\n }),\n deploymentProviders: tenantCatalog.deploymentProviders,\n listCustomProviders: customProviders\n ? async () => {\n await customProviders.ensureReady();\n return customProviders.list({ orgId });\n }\n : undefined,\n })\n ).map(model => model.id);\n for (const id of usable) known.add(id);\n return { usable, known, scopeLabel: modelScope === 'factory' ? 'Factory' : 'session owner' };\n }\n\n function unrunnableModelError(\n modelId: string,\n catalog: { usable: string[]; known: Set<string>; scopeLabel: string },\n ) {\n const options = catalog.usable.length\n ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(', ')}${catalog.usable.length > LISTED_MODELS ? ', …' : ''}.`\n : ' No models are available to it; connect a provider credential first.';\n return new Error(\n catalog.known.has(modelId)\n ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}`\n : `Unknown model ${modelId}.${options}`,\n );\n }\n\n const switchModel = createTool({\n id: 'factory_switch_session_model',\n description:\n 'Look up or switch the model of a session you own in this Factory (side, work item, or review session). When someone describes a model by purpose, family, or preference rather than an exact ID, call with sessionId alone first to list models the target session can actually use. Interpret their intent against that list, then call with the chosen exact modelId to switch; do not use their words as a guessed model ID. A running session switches immediately; a stopped one uses the model on its next run. User-scoped sessions use their owner credentials; Factory-scoped sessions use Factory credentials. Use a session ID from the Factory session or work item tools.',\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200),\n modelId: z.string().trim().min(1).max(200).optional().describe('Omit to list usable models without switching.'),\n })\n .strict(),\n execute: async ({ sessionId, modelId }) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.userId !== scope.userId\n ) {\n throw new Error('Session not found or not owned by you in this Factory.');\n }\n // A turn nobody signed in started reaches only this session and the side sessions it started.\n if (!scope.interactive && sessionId !== scope.sessionId && row.originSessionId !== scope.sessionId) {\n throw new Error(\n 'Without a person in this turn, only this session and the side sessions it started can switch models.',\n );\n }\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session model switching is not available on this server.');\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('Session has no conversation thread yet.');\n // The thread's resource, not the session id: sessions share their Factory's or owner's resource.\n const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;\n const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);\n if (modelId === undefined) return { sessionId, models: catalog.usable };\n if (!catalog.usable.includes(modelId)) throw unrunnableModelError(modelId, catalog);\n const { live } = await messaging.controller.setThreadModel({ resourceId, threadId, modelId });\n return { sessionId, threadId, modelId, live };\n },\n });\n\n const start = createTool({\n id: 'factory_start_session',\n description: [\n 'Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.',\n 'A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.',\n 'Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.',\n 'With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.',\n \"A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to start it from a signed-in chat.\",\n ].join(' '),\n inputSchema: z\n .object({\n prompt: z.string().trim().min(1).max(8000).describe('The first message the new session receives.'),\n title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),\n model: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe(\n 'Model id for a side session. Omit it to inherit this session’s model. The session credentials must be able to run it.',\n ),\n workItem: workItemRefSchema.optional(),\n role: z.string().trim().min(1).max(64).optional(),\n })\n .strict()\n .refine(input => input.role === undefined || input.workItem !== undefined, {\n message: 'Pass workItem together with role.',\n path: ['role'],\n }),\n execute: async ({ prompt, title, model, workItem, role }) => {\n if (workItem !== undefined) {\n // A role start records a run as the person, so it waits for a turn the person started.\n if (!scope.interactive) {\n throw new Error(\n 'Starting a role on a work item needs a person in this turn; start a side session, or ask the person to ask for it in a signed-in chat.',\n );\n }\n return startWorkItemRole({ prompt, title, workItem, role });\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n\n // The child runs with the same credentials as this session, so validate against those.\n const parentThreadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId: scope.sessionId,\n });\n const parentResourceId = parentThreadId\n ? ((await messaging.controller.queryThreadById({ threadId: parentThreadId }))?.resourceId ?? scope.sessionId)\n : scope.sessionId;\n const catalog = await sessionModelCatalog(\n messaging.controller,\n scope.sessionId,\n parentThreadId ?? scope.threadId,\n parentResourceId,\n );\n const inherited = model\n ? null\n : await sessionModel(messaging.controller, deps.sessions, deps.workItems, scope.sessionId);\n const requestedModel = model ?? (inherited && catalog.usable.includes(inherited) ? inherited : undefined);\n if (requestedModel && !catalog.usable.includes(requestedModel))\n throw unrunnableModelError(requestedModel, catalog);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n // Bills the caller: the credential tenant follows the row's user.\n attributeToUserId: scope.userId,\n // Same repository as the session that started it.\n ...(parent.projectRepositoryId ? { projectRepositoryId: parent.projectRepositoryId } : {}),\n visibility: parent.visibility,\n origin: { sessionId: scope.sessionId, threadId: scope.threadId },\n });\n const resolvedModel = await resolveFactorySessionModel({\n ...deps.sessionModel,\n factoryProjectId: scope.factoryProjectId,\n orgId: scope.orgId,\n ownerUserId: scope.userId,\n // No work item, so an interactive session: a hybrid Factory bills it as one.\n context: 'session',\n });\n\n // Only the new row's owner as `user`. The tool call's context carries the\n // interactive marker and the parent's controller entry; neither may reach the child.\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n\n const tags = { factoryProjectId: scope.factoryProjectId, projectRepositoryId: ensured.projectRepositoryId };\n const session = await messaging.controller.createSession({\n id: ensured.sessionId,\n ownerId: scope.userId,\n // A side session works for the Factory, so it shares the Factory's memory\n // resource; a private one stays with its owner, like a Slack DM.\n resourceId: parent.visibility === 'private' ? scope.userId : scope.factoryProjectId,\n threadId: ensured.sessionId,\n requestContext: kickoffContext,\n tags,\n });\n await session.state.set(tags);\n await hydrateFactorySession(session, {\n orgId: scope.orgId,\n modelScope: resolvedModel.modelScope,\n ownerUserId: scope.userId,\n defaultModelId: requestedModel ?? resolvedModel.modelId,\n });\n\n const threadId = session.thread.requireId();\n const threadTitle = title ?? titleFrom(prompt);\n await session.thread.rename({ title: threadTitle, pin: false });\n // On the row too, so its run-end reports name it without waiting for the title mirror.\n await sourceControl.sessions.rename({ sessionId: ensured.sessionId, title: threadTitle });\n await session.thread.setSetting({ key: 'factorySessionId', value: ensured.sessionId });\n\n await primeTenantCredentialsForTenant({\n orgId: scope.orgId,\n userId: scope.userId,\n credentialScope: resolvedModel.modelScope,\n });\n // How its runs end reaches this session through the listener every Factory\n // session gets (`observeSessionRunReports`), which follows the row's origin.\n // `requireDelivery` makes `accepted` reject when the wake fails; the run itself is not awaited.\n await session.sendSignal(\n { id: globalThis.crypto.randomUUID(), type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n\n return {\n mode: 'side' as const,\n sessionId: ensured.sessionId,\n threadId,\n title: threadTitle,\n url:\n factorySessionUrl({ factoryProjectId: scope.factoryProjectId, sessionId: ensured.sessionId, threadId }) ??\n null,\n };\n },\n });\n\n async function startWorkItemRole({\n prompt,\n title,\n workItem,\n role,\n }: {\n prompt: string;\n title?: string;\n workItem: string;\n role?: string;\n }) {\n if (!role) throw new Error('Pass the role to start on the work item.');\n const coordinator = deps.startCoordinator();\n if (!coordinator) throw new Error(\"Factory sessions can't be started yet; try again shortly.\");\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const label = workItemLabel(item);\n const board = deps.boards.get(boardForWorkItem(item));\n if (!board?.phaseForRole(role)) throw new Error(`${label}'s board has no ${role} role.`);\n const seated = item.sessions[role];\n if (seated) {\n throw new Error(\n `${label} already has a ${role} session ${seated.sessionId}; message it with factory_signal_session.`,\n );\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n const project = await deps.sessionModel.projects.get({ orgId: scope.orgId, id: scope.factoryProjectId });\n if (!project) throw new Error('This Factory no longer exists.');\n // A work-item start is a run, so a hybrid Factory uses the Factory's scope (as the Start route does).\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n const projectRepositoryId = await resolveRepository(sourceControl, item);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n projectRepositoryId,\n attributeToUserId: scope.userId,\n // A run works a shared card, so the org can open it and answer its approvals even when the session that\n // started it is private.\n visibility: 'org',\n });\n\n // Follow first, so a fast run's first report already has somewhere to go.\n const followScope = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const alreadyFollowing = (\n await deps.workItems.listSessionSubscriptions({ ...followScope, sessionId: scope.sessionId })\n ).some(subscription => subscription.workItemId === item.id);\n await deps.workItems.subscribeToWorkItem({\n ...followScope,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason: 'started',\n });\n\n const startedAt = new Date();\n let prepared;\n try {\n prepared = await coordinator.prepare({\n orgId: scope.orgId,\n userId: scope.userId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: ensured.sessionId,\n threadTitle: title ?? `${label} ${role}`,\n kickoffKey: globalThis.crypto.randomUUID(),\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n workItem: {\n id: item.id,\n role,\n input: {\n externalSource: item.externalSource,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n stages: item.stages,\n sessions: item.sessions,\n metadata: item.metadata,\n },\n },\n });\n } catch (error) {\n if (!alreadyFollowing) {\n await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n }\n const message = startErrorMessage(error);\n if (message) throw new Error(message);\n throw error;\n }\n\n const bindings = await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const replaced = bindings.find(\n binding =>\n binding.role === role &&\n binding.id !== prepared.bindingId &&\n binding.revokedAt !== null &&\n binding.revokedAt >= startedAt,\n );\n\n const session = await messaging.controller.getSession(prepared.sessionId);\n if (!session) {\n throw new Error(\n `The ${role} session ${prepared.sessionId} was bound but is not running; message it with factory_signal_session to wake it.`,\n );\n }\n const kickoffId = globalThis.crypto.randomUUID();\n // Heartbeat from before the kickoff so a fast run's end is seen; the open-run\n // entry and audit follow the accepted kickoff, as the dispatcher records them.\n // The run's end reaches this item's followers through the session's listener.\n const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n try {\n await session.sendSignal(\n { id: kickoffId, type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n } catch (error) {\n heartbeat.stop();\n throw error;\n }\n\n await recordSessionRunStart(session, {\n audit: deps.auditRecorder,\n actorType: 'human',\n observedEnd: heartbeat.endReason,\n run: {\n kickoffId,\n ownerId: FACTORY_TOOLS_OWNER_ID,\n heartbeatAt: Date.now(),\n bindingId: prepared.bindingId,\n role,\n startedBy: scope.userId,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: item.id,\n workItemName: item.title,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n branch: prepared.branch,\n },\n });\n\n const number = workItemNumber(item);\n return {\n mode: 'work-item' as const,\n workItem: {\n id: item.id,\n ...(number === null ? {} : { number }),\n url:\n factoryWorkItemUrl({\n factoryProjectId: scope.factoryProjectId,\n board: boardForWorkItem(item),\n workItemId: item.id,\n }) ?? null,\n },\n role,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n url:\n factorySessionUrl({\n factoryProjectId: scope.factoryProjectId,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n }) ?? null,\n bindingId: prepared.bindingId,\n ...(replaced ? { replacedSessionId: replaced.sessionId } : {}),\n };\n }\n\n async function resolveRepository(sourceControl: SourceControlStorageHandle, item: WorkItemRow): Promise<string> {\n const intakeConfig = await deps.intake?.getConfig({ orgId: scope.orgId, integrationIds: ['linear'] });\n const repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n throw new Error(ambiguousRepository(repository.candidates));\n }\n if (repository.status === 'unlinked') throw new Error(repository.hint);\n return repository.projectRepositoryId;\n }\n\n return { factory_start_session: start, factory_switch_session_model: switchModel };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsCA,MAAM,eAAe;;AAGrB,MAAa,yBAAyB,iBAAiB,WAAW,OAAO,WAAW;AAwCpF,MAAM,gBAAgB;AAEtB,SAAS,oBAAoB,YAAuC;CAClE,OAAO,uCAAuC,WAAW,KAAK,IAAI,EAAE;AACtE;AAEA,SAAS,kBAAkB,OAA+B;CACxD,IAAI,iBAAiB,yBACnB,OAAO,MAAM,SAAS,yBAAyB,oBAAoB,MAAM,UAAU,IAAI,MAAM;CAE/F,IAAI,iBAAiB,6BAA6B,OAAO,0BAA0B,MAAM,OAAO;CAChG,OAAO;AACT;AAEA,SAAS,cAAc,MAA2B;CAChD,MAAM,SAAS,eAAe,IAAI;CAClC,OAAO,WAAW,OAAO,IAAI,KAAK,MAAM,KAAK,IAAI;AACnD;AAEA,SAAS,UAAU,QAAwB;CAEzC,QADkB,OAAO,MAAM,IAAI,CAAC,CAAC,MAAK,SAAQ,KAAK,KAAK,CAAC,KAAK,OAAA,CACjD,KAAK,CAAC,CAAC,MAAM,GAAG,YAAY;AAC/C;;AAGA,SAAgB,0BACd,OACA,MACkB;;CAElB,eAAe,oBACb,YACA,WACA,UACA,YACuE;EACvE,MAAM,SAAS,MAAM,WAAW,oBAAoB;EACpD,MAAM,QAAQ,IAAI,IAAI,OAAO,SAAQ,UAAU,OAAO,MAAM,OAAO,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAE,CAAC;EAC/F,MAAM,gBAAgB,KAAK,aAAa;EACxC,IAAI,CAAC,eAEH,OAAO;GAAE,SADO,MAAM,kBAAkB,EAAE,WAAW,CAAC,EAAA,CAAG,KAAI,UAAS,MAAM,EAC9D;GAAG;GAAO,YAAY;EAAS;EAE/C,MAAM,iBAAiB,IAAI,eAAe;EAC1C,eAAe,IAAI,QAAQ;GAAE,IAAI,MAAM;GAAQ,gBAAgB,MAAM;EAAM,CAAC;EAC5E,eAAe,IAAI,cAAc;GAAE;GAAW;GAAY;EAAS,CAAC;EACpE,MAAM,SAAS,MAAM,0BAA0B,gBAAgB;GAC7D,UAAU,KAAK,aAAa;GAC5B,eAAe,KAAK;GACpB,WAAW,KAAK;EAClB,CAAC;EACD,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,sCAAsC;EACnE,MAAM,aAAa,OAAO,OAAO,mBAAmB;EACpD,MAAM,QAAQ,OAAO,OAAO,SAAS,MAAM;EAC3C,MAAM,kBAAkB,cAAc;EACtC,MAAM,UACJ,MAAM,kBAAkB;GACtB;GACA,mBAAmB,MAAM,6BAA6B;IACpD,aAAa,cAAc;IAC3B;IACA,QAAQ,OAAO,OAAO;IACtB;GACF,CAAC;GACD,qBAAqB,cAAc;GACnC,qBAAqB,kBACjB,YAAY;IACV,MAAM,gBAAgB,YAAY;IAClC,OAAO,gBAAgB,KAAK,EAAE,MAAM,CAAC;GACvC,IACA,KAAA;EACN,CAAC,EAAA,CACD,KAAI,UAAS,MAAM,EAAE;EACvB,KAAK,MAAM,MAAM,QAAQ,MAAM,IAAI,EAAE;EACrC,OAAO;GAAE;GAAQ;GAAO,YAAY,eAAe,YAAY,YAAY;EAAgB;CAC7F;CAEA,SAAS,qBACP,SACA,SACA;EACA,MAAM,UAAU,QAAQ,OAAO,SAC3B,4BAA4B,QAAQ,OAAO,MAAM,GAAG,aAAa,CAAC,CAAC,KAAK,IAAI,IAAI,QAAQ,OAAO,SAAS,gBAAgB,QAAQ,GAAG,KACnI;EACJ,uBAAO,IAAI,MACT,QAAQ,MAAM,IAAI,OAAO,IACrB,SAAS,QAAQ,gDAAgD,QAAQ,WAAW,uCAAuC,YAC3H,iBAAiB,QAAQ,GAAG,SAClC;CACF;CAEA,MAAM,cAAc,WAAW;EAC7B,IAAI;EACJ,aACE;EACF,aAAa,EACV,OAAO;GACN,WAAW,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG;GAC3C,SAAS,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,+CAA+C;EAChH,CAAC,CAAC,CACD,OAAO;EACV,SAAS,OAAO,EAAE,WAAW,cAAc;GACzC,MAAM,MAAM,MAAM,KAAK,SAAS,eAAe,SAAS;GACxD,IACE,CAAC,OACD,IAAI,UAAU,MAAM,SACpB,IAAI,qBAAqB,MAAM,oBAC/B,IAAI,WAAW,MAAM,QAErB,MAAM,IAAI,MAAM,wDAAwD;GAG1E,IAAI,CAAC,MAAM,eAAe,cAAc,MAAM,aAAa,IAAI,oBAAoB,MAAM,WACvF,MAAM,IAAI,MACR,sGACF;GAEF,MAAM,YAAY,MAAM,KAAK,UAAU;GACvC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,0DAA0D;GAC1F,MAAM,WAAW,MAAM,qBAAqB;IAC1C,YAAY,UAAU;IACtB,UAAU,KAAK;IACf,WAAW,KAAK;IAChB;GACF,CAAC;GACD,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,yCAAyC;GAExE,MAAM,cAAc,MAAM,UAAU,WAAW,gBAAgB,EAAE,SAAS,CAAC,EAAA,EAAI,cAAc;GAC7F,MAAM,UAAU,MAAM,oBAAoB,UAAU,YAAY,WAAW,UAAU,UAAU;GAC/F,IAAI,YAAY,KAAA,GAAW,OAAO;IAAE;IAAW,QAAQ,QAAQ;GAAO;GACtE,IAAI,CAAC,QAAQ,OAAO,SAAS,OAAO,GAAG,MAAM,qBAAqB,SAAS,OAAO;GAClF,MAAM,EAAE,SAAS,MAAM,UAAU,WAAW,eAAe;IAAE;IAAY;IAAU;GAAQ,CAAC;GAC5F,OAAO;IAAE;IAAW;IAAU;IAAS;GAAK;EAC9C;CACF,CAAC;CAED,MAAM,QAAQ,WAAW;EACvB,IAAI;EACJ,aAAa;GACX;GACA;GACA;GACA;GACA;EACF,CAAC,CAAC,KAAK,GAAG;EACV,aAAa,EACV,OAAO;GACN,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAI,CAAC,CAAC,SAAS,6CAA6C;GACjG,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,SAAS;GAC3D,OAAO,EACJ,OAAO,CAAC,CACR,KAAK,CAAC,CACN,IAAI,CAAC,CAAC,CACN,IAAI,GAAG,CAAC,CACR,SAAS,CAAC,CACV,SACC,uHACF;GACF,UAAU,kBAAkB,SAAS;GACrC,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS;EAClD,CAAC,CAAC,CACD,OAAO,CAAC,CACR,QAAO,UAAS,MAAM,SAAS,KAAA,KAAa,MAAM,aAAa,KAAA,GAAW;GACzE,SAAS;GACT,MAAM,CAAC,MAAM;EACf,CAAC;EACH,SAAS,OAAO,EAAE,QAAQ,OAAO,OAAO,UAAU,WAAW;GAC3D,IAAI,aAAa,KAAA,GAAW;IAE1B,IAAI,CAAC,MAAM,aACT,MAAM,IAAI,MACR,wIACF;IAEF,OAAO,kBAAkB;KAAE;KAAQ;KAAO;KAAU;IAAK,CAAC;GAC5D;GAEA,MAAM,SAAS,MAAM,KAAK,SAAS,eAAe,MAAM,SAAS;GACjE,MAAM,gBAAgB,MAAM,KAAK,SAAS,4BAA4B,MAAM,SAAS;GACrF,IAAI,CAAC,UAAU,CAAC,eAAe,MAAM,IAAI,MAAM,wDAAwD;GACvG,MAAM,YAAY,MAAM,KAAK,UAAU;GACvC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,oDAAoD;GAGpF,MAAM,iBAAiB,MAAM,qBAAqB;IAChD,YAAY,UAAU;IACtB,UAAU,KAAK;IACf,WAAW,KAAK;IAChB,WAAW,MAAM;GACnB,CAAC;GACD,MAAM,mBAAmB,kBACnB,MAAM,UAAU,WAAW,gBAAgB,EAAE,UAAU,eAAe,CAAC,EAAA,EAAI,cAAc,MAAM,YACjG,MAAM;GACV,MAAM,UAAU,MAAM,oBACpB,UAAU,YACV,MAAM,WACN,kBAAkB,MAAM,UACxB,gBACF;GACA,MAAM,YAAY,QACd,OACA,MAAM,aAAa,UAAU,YAAY,KAAK,UAAU,KAAK,WAAW,MAAM,SAAS;GAC3F,MAAM,iBAAiB,UAAU,aAAa,QAAQ,OAAO,SAAS,SAAS,IAAI,YAAY,KAAA;GAC/F,IAAI,kBAAkB,CAAC,QAAQ,OAAO,SAAS,cAAc,GAC3D,MAAM,qBAAqB,gBAAgB,OAAO;GAEpD,MAAM,YAAY,WAAW,OAAO,WAAW;GAC/C,MAAM,UAAU,MAAM,2BAA2B;IAC/C;IACA,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB;IACA,QAAQ,GAAG,6BAA6B;IAExC,mBAAmB,MAAM;IAEzB,GAAI,OAAO,sBAAsB,EAAE,qBAAqB,OAAO,oBAAoB,IAAI,CAAC;IACxF,YAAY,OAAO;IACnB,QAAQ;KAAE,WAAW,MAAM;KAAW,UAAU,MAAM;IAAS;GACjE,CAAC;GACD,MAAM,gBAAgB,MAAM,2BAA2B;IACrD,GAAG,KAAK;IACR,kBAAkB,MAAM;IACxB,OAAO,MAAM;IACb,aAAa,MAAM;IAEnB,SAAS;GACX,CAAC;GAID,MAAM,iBAAiB,IAAI,eAAe;GAC1C,eAAe,IAAI,QAAQ;IAAE,UAAU,MAAM;IAAQ,gBAAgB,MAAM;GAAM,CAAC;GAElF,MAAM,OAAO;IAAE,kBAAkB,MAAM;IAAkB,qBAAqB,QAAQ;GAAoB;GAC1G,MAAM,UAAU,MAAM,UAAU,WAAW,cAAc;IACvD,IAAI,QAAQ;IACZ,SAAS,MAAM;IAGf,YAAY,OAAO,eAAe,YAAY,MAAM,SAAS,MAAM;IACnE,UAAU,QAAQ;IAClB,gBAAgB;IAChB;GACF,CAAC;GACD,MAAM,QAAQ,MAAM,IAAI,IAAI;GAC5B,MAAM,sBAAsB,SAAS;IACnC,OAAO,MAAM;IACb,YAAY,cAAc;IAC1B,aAAa,MAAM;IACnB,gBAAgB,kBAAkB,cAAc;GAClD,CAAC;GAED,MAAM,WAAW,QAAQ,OAAO,UAAU;GAC1C,MAAM,cAAc,SAAS,UAAU,MAAM;GAC7C,MAAM,QAAQ,OAAO,OAAO;IAAE,OAAO;IAAa,KAAK;GAAM,CAAC;GAE9D,MAAM,cAAc,SAAS,OAAO;IAAE,WAAW,QAAQ;IAAW,OAAO;GAAY,CAAC;GACxF,MAAM,QAAQ,OAAO,WAAW;IAAE,KAAK;IAAoB,OAAO,QAAQ;GAAU,CAAC;GAErF,MAAM,gCAAgC;IACpC,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,iBAAiB,cAAc;GACjC,CAAC;GAID,MAAM,QAAQ,WACZ;IAAE,IAAI,WAAW,OAAO,WAAW;IAAG,MAAM;IAAQ,SAAS;IAAQ,UAAU;GAAO,GACtF;IAAE,gBAAgB;IAAgB,iBAAiB;GAAK,CAC1D,CAAC,CAAC;GAEF,OAAO;IACL,MAAM;IACN,WAAW,QAAQ;IACnB;IACA,OAAO;IACP,KACE,kBAAkB;KAAE,kBAAkB,MAAM;KAAkB,WAAW,QAAQ;KAAW;IAAS,CAAC,KACtG;GACJ;EACF;CACF,CAAC;CAED,eAAe,kBAAkB,EAC/B,QACA,OACA,UACA,QAMC;EACD,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,0CAA0C;EACrE,MAAM,cAAc,KAAK,iBAAiB;EAC1C,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,2DAA2D;EAC7F,MAAM,OAAO,MAAM,aAAa,KAAK,WAAW,OAAO,QAAQ;EAC/D,IAAI,CAAC,MAAM,MAAM,iBAAiB,QAAQ;EAC1C,MAAM,QAAQ,cAAc,IAAI;EAEhC,IAAI,CADU,KAAK,OAAO,IAAI,iBAAiB,IAAI,CAC1C,CAAC,EAAE,aAAa,IAAI,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,kBAAkB,KAAK,OAAO;EACvF,MAAM,SAAS,KAAK,SAAS;EAC7B,IAAI,QACF,MAAM,IAAI,MACR,GAAG,MAAM,iBAAiB,KAAK,WAAW,OAAO,UAAU,0CAC7D;EAGF,MAAM,SAAS,MAAM,KAAK,SAAS,eAAe,MAAM,SAAS;EACjE,MAAM,gBAAgB,MAAM,KAAK,SAAS,4BAA4B,MAAM,SAAS;EACrF,IAAI,CAAC,UAAU,CAAC,eAAe,MAAM,IAAI,MAAM,wDAAwD;EACvG,MAAM,YAAY,MAAM,KAAK,UAAU;EACvC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,oDAAoD;EACpF,MAAM,UAAU,MAAM,KAAK,aAAa,SAAS,IAAI;GAAE,OAAO,MAAM;GAAO,IAAI,MAAM;EAAiB,CAAC;EACvG,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gCAAgC;EAE9D,MAAM,aAAa,oBAAoB,QAAQ,YAAY,KAAK;EAChE,MAAM,sBAAsB,MAAM,kBAAkB,eAAe,IAAI;EAEvE,MAAM,YAAY,WAAW,OAAO,WAAW;EAC/C,MAAM,UAAU,MAAM,2BAA2B;GAC/C;GACA,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB;GACA,QAAQ,GAAG,6BAA6B;GACxC;GACA,mBAAmB,MAAM;GAGzB,YAAY;EACd,CAAC;EAGD,MAAM,cAAc;GAAE,OAAO,MAAM;GAAO,kBAAkB,MAAM;EAAiB;EACnF,MAAM,oBACJ,MAAM,KAAK,UAAU,yBAAyB;GAAE,GAAG;GAAa,WAAW,MAAM;EAAU,CAAC,EAAA,CAC5F,MAAK,iBAAgB,aAAa,eAAe,KAAK,EAAE;EAC1D,MAAM,KAAK,UAAU,oBAAoB;GACvC,GAAG;GACH,YAAY,KAAK;GACjB,WAAW,MAAM;GACjB,UAAU,MAAM;GAChB,YAAY,MAAM;GAClB,QAAQ,MAAM;GACd,QAAQ;EACV,CAAC;EAED,MAAM,4BAAY,IAAI,KAAK;EAC3B,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,YAAY,QAAQ;IACnC,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,kBAAkB,MAAM;IACxB,WAAW,QAAQ;IACnB,aAAa,SAAS,GAAG,MAAM,GAAG;IAClC,YAAY,WAAW,OAAO,WAAW;IACzC;IACA,gBAAgB,eAAe,YAAa,QAAQ,kBAAkB,KAAA,IAAa,KAAA;IACnF,UAAU;KACR,IAAI,KAAK;KACT;KACA,OAAO;MACL,gBAAgB,KAAK;MACrB,kBAAkB,KAAK;MACvB,OAAO,KAAK;MACZ,QAAQ,KAAK;MACb,UAAU,KAAK;MACf,UAAU,KAAK;KACjB;IACF;GACF,CAAC;EACH,SAAS,OAAO;GACd,IAAI,CAAC,kBACH,MAAM,KAAK,UAAU,wBAAwB;IAC3C,OAAO,MAAM;IACb,YAAY,KAAK;IACjB,WAAW,MAAM;GACnB,CAAC;GAEH,MAAM,UAAU,kBAAkB,KAAK;GACvC,IAAI,SAAS,MAAM,IAAI,MAAM,OAAO;GACpC,MAAM;EACR;EAGA,MAAM,YAAW,MADM,KAAK,UAAU,gBAAgB,MAAM,OAAO,MAAM,kBAAkB,KAAK,EAAE,EAAA,CACxE,MACxB,YACE,QAAQ,SAAS,QACjB,QAAQ,OAAO,SAAS,aACxB,QAAQ,cAAc,QACtB,QAAQ,aAAa,SACzB;EAEA,MAAM,UAAU,MAAM,UAAU,WAAW,WAAW,SAAS,SAAS;EACxE,IAAI,CAAC,SACH,MAAM,IAAI,MACR,OAAO,KAAK,WAAW,SAAS,UAAU,kFAC5C;EAEF,MAAM,YAAY,WAAW,OAAO,WAAW;EAI/C,MAAM,YAAY,yBAAyB,SAAS,SAAS;EAC7D,MAAM,iBAAiB,IAAI,eAAe;EAC1C,eAAe,IAAI,QAAQ;GAAE,UAAU,MAAM;GAAQ,gBAAgB,MAAM;EAAM,CAAC;EAClF,IAAI;GACF,MAAM,QAAQ,WACZ;IAAE,IAAI;IAAW,MAAM;IAAQ,SAAS;IAAQ,UAAU;GAAO,GACjE;IAAE,gBAAgB;IAAgB,iBAAiB;GAAK,CAC1D,CAAC,CAAC;EACJ,SAAS,OAAO;GACd,UAAU,KAAK;GACf,MAAM;EACR;EAEA,MAAM,sBAAsB,SAAS;GACnC,OAAO,KAAK;GACZ,WAAW;GACX,aAAa,UAAU;GACvB,KAAK;IACH;IACA,SAAS;IACT,aAAa,KAAK,IAAI;IACtB,WAAW,SAAS;IACpB;IACA,WAAW,MAAM;IACjB,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,YAAY,KAAK;IACjB,cAAc,KAAK;IACnB,WAAW,SAAS;IACpB,UAAU,SAAS;IACnB,QAAQ,SAAS;GACnB;EACF,CAAC;EAED,MAAM,SAAS,eAAe,IAAI;EAClC,OAAO;GACL,MAAM;GACN,UAAU;IACR,IAAI,KAAK;IACT,GAAI,WAAW,OAAO,CAAC,IAAI,EAAE,OAAO;IACpC,KACE,mBAAmB;KACjB,kBAAkB,MAAM;KACxB,OAAO,iBAAiB,IAAI;KAC5B,YAAY,KAAK;IACnB,CAAC,KAAK;GACV;GACA;GACA,WAAW,SAAS;GACpB,UAAU,SAAS;GACnB,KACE,kBAAkB;IAChB,kBAAkB,MAAM;IACxB,WAAW,SAAS;IACpB,UAAU,SAAS;GACrB,CAAC,KAAK;GACR,WAAW,SAAS;GACpB,GAAI,WAAW,EAAE,mBAAmB,SAAS,UAAU,IAAI,CAAC;EAC9D;CACF;CAEA,eAAe,kBAAkB,eAA2C,MAAoC;EAC9G,MAAM,eAAe,MAAM,KAAK,QAAQ,UAAU;GAAE,OAAO,MAAM;GAAO,gBAAgB,CAAC,QAAQ;EAAE,CAAC;EACpG,MAAM,aAAa,MAAM,0BAA0B;GACjD;GACA,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB;GACA,qBAAqB,cAAc,QAAQ;EAC7C,CAAC;EACD,IAAI,WAAW,WAAW,aACxB,MAAM,IAAI,MAAM,oBAAoB,WAAW,UAAU,CAAC;EAE5D,IAAI,WAAW,WAAW,YAAY,MAAM,IAAI,MAAM,WAAW,IAAI;EACrE,OAAO,WAAW;CACpB;CAEA,OAAO;EAAE,uBAAuB;EAAO,8BAA8B;CAAY;AACnF"}
@@ -1 +1 @@
1
- {"version":3,"file":"work-item-tools.d.ts","sourceRoot":"","sources":["../../src/tools/work-item-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC/E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,KAAK,EAAoC,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAmBhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,oFAAoF;IACpF,iBAAiB,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,GAAG,SAAS,CAAC;IAC5E,kGAAkG;IAClG,aAAa,EAAE,aAAa,CAAC;IAC7B,4FAA4F;IAC5F,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAC5C,qGAAqG;IACrG,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,6BAA6B,CAAC,CAAC;CAC3E;AA2BD,8EAA8E;AAC9E,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,+BAA+B,GACpC,gBAAgB,CAgTlB"}
1
+ {"version":3,"file":"work-item-tools.d.ts","sourceRoot":"","sources":["../../src/tools/work-item-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC/E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,KAAK,EAAoC,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAmBhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,oFAAoF;IACpF,iBAAiB,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,GAAG,SAAS,CAAC;IAC5E,kGAAkG;IAClG,aAAa,EAAE,aAAa,CAAC;IAC7B,4FAA4F;IAC5F,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAC5C,qGAAqG;IACrG,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,6BAA6B,CAAC,CAAC;CAC3E;AAiCD,8EAA8E;AAC9E,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,+BAA+B,GACpC,gBAAgB,CAgTlB"}
@@ -2,6 +2,7 @@ import { boardForWorkItem } from "../boards/semantics.js";
2
2
  import "../boards/index.js";
3
3
  import { WorkItemRelationError } from "../storage/domains/work-items/base.js";
4
4
  import { requirePerson } from "./scope.js";
5
+ import { factoryWorkItemUrl } from "../links.js";
5
6
  import { findWorkItem, truncate, workItemNotFound, workItemNumber, workItemRefSchema, workItemStage } from "./read-tools.js";
6
7
  import { transitionBodySchema } from "../routes/contracts.js";
7
8
  import { resolveWorkItemRepository } from "../session/work-item-repository.js";
@@ -23,7 +24,12 @@ function briefItem(item) {
23
24
  id: item.id,
24
25
  number: workItemNumber(item),
25
26
  title: truncate(item.title, 600),
26
- stage: workItemStage(item)
27
+ stage: workItemStage(item),
28
+ url: factoryWorkItemUrl({
29
+ factoryProjectId: item.factoryProjectId,
30
+ board: boardForWorkItem(item),
31
+ workItemId: item.id
32
+ }) ?? null
27
33
  };
28
34
  }
29
35
  function settled(result) {
@@ -1 +1 @@
1
- {"version":3,"file":"work-item-tools.js","names":[],"sources":["../../src/tools/work-item-tools.ts"],"sourcesContent":["/**\n * Factory tools that change work items: file one, move it, settle its\n * decisions, and follow it. They act as the session's person (a human actor),\n * through the same code the board's routes use.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { transitionBodySchema } from '../routes/contracts.js';\nimport type { FactoryTransitionService } from '../rules/transition-service.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { CommentsDomain } from '../storage/domains/comments/domain.js';\nimport { WorkItemRelationError } from '../storage/domains/work-items/base.js';\nimport type { UpdateWorkItemInput, WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { createFactoryWorkItem } from '../work-items/create-work-item.js';\nimport type { FactoryAuditEmit } from '../work-items/create-work-item.js';\nimport {\n dismissFactoryDecision,\n resolveFactoryProposal,\n retryFactoryDecision,\n} from '../work-items/decision-actions.js';\nimport type { FactoryDecisionActionResult } from '../work-items/decision-actions.js';\nimport {\n MAX_TEXT,\n findWorkItem,\n workItemRefSchema,\n truncate,\n workItemNotFound,\n workItemNumber,\n workItemStage,\n} from './read-tools.js';\nimport { requirePerson } from './scope.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport interface FactoryWorkItemToolDependencies {\n workItems: WorkItemsStorage;\n boards: BoardRegistry;\n /** Governed stage moves; filing and moving items refuse while it is unavailable. */\n transitionService: Pick<FactoryTransitionService, 'transition'> | undefined;\n /** Writes audit rows with an explicit actor; tool calls have no HTTP request to read one from. */\n auditRecorder: AuditRecorder;\n /** The feed's front door: validates, touches open boards and mirrors to linked channels. */\n feed: Pick<CommentsDomain, 'createComment'>;\n /** Finds the caller session's source-control provider, whose linked repositories a card can name. */\n sessions: Pick<SourceControlSessionLookup, 'getSourceControlBySessionId'>;\n}\n\nconst repositorySchema = z\n .string()\n .trim()\n .min(3)\n .max(200)\n .regex(/^[^/\\s]+\\/[^/\\s]+$/, 'Use the owner/name form, e.g. \"mastra-ai/mastra\".')\n .describe('A repository linked to this Factory, as owner/name.');\n\nconst decisionId = z.string().trim().uuid().describe('Decision id, from factory_list_attention or inspect.');\n\nfunction briefItem(item: WorkItemRow) {\n return {\n id: item.id,\n number: workItemNumber(item),\n title: truncate(item.title, MAX_TEXT),\n stage: workItemStage(item),\n };\n}\n\nfunction settled(result: FactoryDecisionActionResult) {\n if (!result.ok) throw new Error(result.message);\n const { decision } = result;\n return { decisionId: decision.id, status: decision.status, workItemId: decision.workItemId };\n}\n\n/** The work-item write and subscription tools for one user session's turn. */\nexport function createFactoryWorkItemTools(\n scope: FactoryToolScope,\n deps: FactoryWorkItemToolDependencies,\n): IntegrationTools {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const audit: FactoryAuditEmit = async input => {\n await deps.auditRecorder.record({\n orgId: scope.orgId,\n // Turns nobody signed in started are recorded as this session's agent, as their moves are.\n actorId: scope.interactive ? scope.userId : `agent:session:${scope.sessionId}`,\n actorType: scope.interactive ? 'human' : 'agent',\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n ...input,\n });\n };\n const requireItem = async (ref: string) => {\n const item = await findWorkItem(deps.workItems, scope, ref);\n if (!item) throw workItemNotFound(ref);\n return item;\n };\n /**\n * Check `slug` is a repository linked to this Factory and that a card with\n * `metadata` would run in it (a card synced from an issue follows the\n * issue's repository, which a slug can't override).\n */\n const requireRepository = async (slug: string, metadata: Record<string, unknown> | null = null) => {\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!sourceControl) throw new Error('This session has no source-control provider to look repositories up in.');\n const resolve = (repository?: string) =>\n resolveWorkItemRepository({\n sourceControl,\n ...project,\n item: { metadata: repository ? { ...metadata, repository } : metadata },\n });\n const resolution = await resolve(slug);\n if (resolution.status === 'resolved' && resolution.slug === slug) return;\n if (resolution.status === 'resolved') {\n throw new Error(`This card runs in ${resolution.slug}, which comes from its linked issue and can't be changed.`);\n }\n const linked = await resolve();\n const candidates =\n linked.status === 'ambiguous' ? linked.candidates : linked.status === 'resolved' ? [linked.slug] : [];\n throw new Error(\n candidates.length > 0\n ? `${slug} is not linked to this Factory. Linked repositories: ${candidates.join(', ')}.`\n : `${slug} is not linked to this Factory, and it has no linked repositories.`,\n );\n };\n const follow = (item: WorkItemRow, reason: 'created' | 'subscribed') =>\n deps.workItems.subscribeToWorkItem({\n ...project,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason,\n });\n\n return {\n factory_create_work_item: createTool({\n id: 'factory_create_work_item',\n description:\n \"File a new work item on this Factory's Work board for the person you are working with. It lands in the board's first phase, exactly like pressing New card, and this session follows it. Check factory_list_work_items for an existing item first. Name the repository when the Factory links more than one; runs refuse to start on a card that doesn't say which.\",\n inputSchema: z\n .object({\n title: z.string().trim().min(1).max(512).describe('Short title, as it should read on the card.'),\n brief: z\n .string()\n .trim()\n .min(1)\n .max(8000)\n .describe('What needs doing and why; posted as the first comment on the card.'),\n repository: repositorySchema.optional(),\n })\n .strict(),\n execute: async ({ title, brief, repository }) => {\n if (repository) await requireRepository(repository);\n const created = await createFactoryWorkItem({\n workItems: deps.workItems,\n boards: deps.boards,\n transitionService: deps.transitionService,\n audit,\n ...project,\n userId: scope.userId,\n ...(scope.actorProfile ? { origin: { actorProfile: scope.actorProfile } } : {}),\n input: { title, ...(repository ? { metadata: { repository } } : {}) },\n });\n if (created.status === 'refused') throw new Error(created.message);\n if (created.status === 'rejected') throw new Error(`The board refused the new item: ${created.reason}`);\n const { item } = created;\n // The card exists from here on: report follow-up failures alongside it\n // rather than failing the call, so the agent doesn't file it twice.\n const warnings: string[] = [];\n try {\n const comment = await deps.feed.createComment({\n orgId: scope.orgId,\n workItemId: item.id,\n author: { kind: 'user', id: scope.userId },\n body: brief,\n });\n if (comment.status !== 'created') {\n warnings.push(\n `The brief was not posted (${comment.status === 'invalid' ? comment.message : comment.status}).`,\n );\n }\n } catch (error) {\n warnings.push(`The brief was not posted: ${error instanceof Error ? error.message : String(error)}`);\n }\n try {\n await follow(item, 'created');\n } catch (error) {\n warnings.push(\n `This session does not follow it yet: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n return warnings.length ? { ...briefItem(item), warnings } : briefItem(item);\n },\n }),\n\n factory_transition_work_item: createTool({\n id: 'factory_transition_work_item',\n description:\n \"Move a work item to another stage of its board, as the person you are working with. The board's rules decide whether the move is allowed and what it starts.\",\n inputSchema: z\n .object({\n workItem: workItemRefSchema,\n stage: z.string().trim().min(1).max(64).describe('Destination stage, e.g. \"planning\" or \"done\".'),\n })\n .strict(),\n execute: async ({ workItem, stage }) => {\n if (!deps.transitionService) throw new Error('Factory stage transitions are not available right now.');\n const item = await requireItem(workItem);\n const parsed = transitionBodySchema.safeParse({\n board: boardForWorkItem(item),\n stage,\n expectedRevision: item.revision,\n requestId: randomUUID(),\n cause: 'factory_tools',\n });\n if (!parsed.success) throw new Error(`\"${stage}\" is not a valid stage name.`);\n const result = await deps.transitionService.transition({\n ...parsed.data,\n ...project,\n workItemId: item.id,\n ...(scope.interactive\n ? {\n actor: { type: 'human' as const, id: scope.userId },\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n ingress: { ...parsed.data.ingress, identity: `human:${scope.userId}:${parsed.data.ingress.identity}` },\n }\n : {\n // Nobody signed in started this turn: move as this session's agent, so the\n // plan gate and the agent's done gates apply and the move arms no runs.\n actor: { type: 'agent' as const, bindingId: `session:${scope.sessionId}`, role: 'session' },\n ingress: {\n type: 'agent' as const,\n identity: `session:${scope.sessionId}:${parsed.data.ingress.identity}`,\n },\n }),\n });\n if (result.status === 'rejected') throw new Error(`The move was refused (${result.code}): ${result.reason}`);\n return { workItemId: item.id, stage: result.stage, revision: result.revision };\n },\n }),\n\n factory_update_work_item: createTool({\n id: 'factory_update_work_item',\n description:\n \"Edit a work item's fields as the person you are working with: its title, the repository its runs use, its parent item, or pre-approving its plans. Move stages with factory_transition_work_item and start roles with factory_start_session; post to the card's feed for anything else.\",\n inputSchema: z\n .object({\n workItem: workItemRefSchema,\n title: z.string().trim().min(1).max(500).optional().describe('New card title.'),\n repository: repositorySchema.optional(),\n parentWorkItem: workItemRefSchema\n .nullable()\n .optional()\n .describe('Parent work item (id or #number), or null to detach it from its parent.'),\n plansPreapproved: z\n .literal(true)\n .optional()\n .describe(\n \"Pre-approve the item's plans, so its runs no longer stop for plan approval. Can't be undone here.\",\n ),\n })\n .strict()\n .refine(input => Object.keys(input).some(key => key !== 'workItem'), 'Pass at least one field to change.'),\n execute: async ({ workItem, title, repository, parentWorkItem, plansPreapproved }) => {\n // Pre-approval drops the plan-approval gate for good, so it is the person's call.\n if (plansPreapproved) requirePerson(scope, 'pre-approve plans');\n const item = await requireItem(workItem);\n if (repository) await requireRepository(repository, item.metadata);\n let parentWorkItemId: string | null | undefined;\n if (parentWorkItem === null) parentWorkItemId = null;\n else if (parentWorkItem !== undefined) parentWorkItemId = (await requireItem(parentWorkItem)).id;\n const patch: UpdateWorkItemInput = {\n ...(title !== undefined ? { title } : {}),\n ...(repository ? { metadata: { repository } } : {}),\n ...(parentWorkItemId !== undefined ? { parentWorkItemId } : {}),\n ...(plansPreapproved ? { plansPreapproved } : {}),\n };\n let updated: Awaited<ReturnType<WorkItemsStorage['update']>>;\n try {\n updated = await deps.workItems.update({ orgId: scope.orgId, id: item.id, userId: scope.userId, patch });\n } catch (error) {\n if (error instanceof WorkItemRelationError) throw new Error(`The parent was refused: ${error.message}`);\n throw error;\n }\n if (!updated) throw workItemNotFound(workItem);\n const fields = [\n ...(title !== undefined ? ['title'] : []),\n ...(repository ? ['metadata'] : []),\n ...(parentWorkItemId !== undefined ? ['parentWorkItemId'] : []),\n ...(plansPreapproved ? ['plansPreapproved'] : []),\n ];\n await audit({\n action: 'factory.work_item.updated',\n factoryProjectId: scope.factoryProjectId,\n targets: [{ type: 'work_item', id: updated.item.id, name: updated.item.title }],\n metadata: { fields, cause: 'factory_tools' },\n });\n const metadata = updated.item.metadata ?? {};\n return {\n workItem: briefItem(updated.item),\n updated: fields.map(field => (field === 'metadata' ? 'repository' : field)),\n repository: typeof metadata.repository === 'string' ? metadata.repository : null,\n parentWorkItemId: updated.item.parentWorkItemId,\n plansPreapproved: updated.item.plansPreapprovedAt !== null,\n };\n },\n }),\n\n factory_retry_decision: createTool({\n id: 'factory_retry_decision',\n description:\n 'Retry one failed Factory run once its cause is fixed. Refused when the failure cannot be fixed by retrying or the item has moved on.',\n inputSchema: z.object({ decisionId }).strict(),\n execute: async ({ decisionId }) =>\n settled(await retryFactoryDecision({ workItems: deps.workItems, boards: deps.boards, ...project, decisionId })),\n }),\n\n factory_dismiss_decision: createTool({\n id: 'factory_dismiss_decision',\n description: 'Dismiss one proposed Factory run that is no longer wanted.',\n inputSchema: z.object({ decisionId }).strict(),\n execute: async ({ decisionId }) => {\n requirePerson(scope, 'dismiss a proposed run');\n return settled(\n await dismissFactoryDecision({\n workItems: deps.workItems,\n audit,\n ...project,\n userId: scope.userId,\n decisionId,\n }),\n );\n },\n }),\n\n factory_resolve_proposal: createTool({\n id: 'factory_resolve_proposal',\n description:\n \"Approve or dismiss one proposed Factory run. Approving starts the run and lets the item's later runs start without asking.\",\n inputSchema: z.object({ decisionId, action: z.enum(['approve', 'dismiss']) }).strict(),\n execute: async ({ decisionId, action }) => {\n requirePerson(scope, 'approve or dismiss a proposed run');\n return settled(\n await resolveFactoryProposal({\n workItems: deps.workItems,\n audit,\n ...project,\n userId: scope.userId,\n decisionId,\n action,\n }),\n );\n },\n }),\n\n factory_subscribe_work_item: createTool({\n id: 'factory_subscribe_work_item',\n description:\n 'Follow a work item from this session, so you hear about its progress and its sessions report to you. Items you file or start are followed already.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await requireItem(workItem);\n const subscription = await follow(item, 'subscribed');\n return { workItem: briefItem(item), subscribed: true, reason: subscription.reason };\n },\n }),\n\n factory_unsubscribe_work_item: createTool({\n id: 'factory_unsubscribe_work_item',\n description: 'Stop following a work item from this session.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await requireItem(workItem);\n const removed = await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n return { workItem: briefItem(item), subscribed: false, wasSubscribed: removed };\n },\n }),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"work-item-tools.js","names":[],"sources":["../../src/tools/work-item-tools.ts"],"sourcesContent":["/**\n * Factory tools that change work items: file one, move it, settle its\n * decisions, and follow it. They act as the session's person (a human actor),\n * through the same code the board's routes use.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { factoryWorkItemUrl } from '../links.js';\nimport { transitionBodySchema } from '../routes/contracts.js';\nimport type { FactoryTransitionService } from '../rules/transition-service.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { CommentsDomain } from '../storage/domains/comments/domain.js';\nimport { WorkItemRelationError } from '../storage/domains/work-items/base.js';\nimport type { UpdateWorkItemInput, WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { createFactoryWorkItem } from '../work-items/create-work-item.js';\nimport type { FactoryAuditEmit } from '../work-items/create-work-item.js';\nimport {\n dismissFactoryDecision,\n resolveFactoryProposal,\n retryFactoryDecision,\n} from '../work-items/decision-actions.js';\nimport type { FactoryDecisionActionResult } from '../work-items/decision-actions.js';\nimport {\n MAX_TEXT,\n findWorkItem,\n workItemRefSchema,\n truncate,\n workItemNotFound,\n workItemNumber,\n workItemStage,\n} from './read-tools.js';\nimport { requirePerson } from './scope.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport interface FactoryWorkItemToolDependencies {\n workItems: WorkItemsStorage;\n boards: BoardRegistry;\n /** Governed stage moves; filing and moving items refuse while it is unavailable. */\n transitionService: Pick<FactoryTransitionService, 'transition'> | undefined;\n /** Writes audit rows with an explicit actor; tool calls have no HTTP request to read one from. */\n auditRecorder: AuditRecorder;\n /** The feed's front door: validates, touches open boards and mirrors to linked channels. */\n feed: Pick<CommentsDomain, 'createComment'>;\n /** Finds the caller session's source-control provider, whose linked repositories a card can name. */\n sessions: Pick<SourceControlSessionLookup, 'getSourceControlBySessionId'>;\n}\n\nconst repositorySchema = z\n .string()\n .trim()\n .min(3)\n .max(200)\n .regex(/^[^/\\s]+\\/[^/\\s]+$/, 'Use the owner/name form, e.g. \"mastra-ai/mastra\".')\n .describe('A repository linked to this Factory, as owner/name.');\n\nconst decisionId = z.string().trim().uuid().describe('Decision id, from factory_list_attention or inspect.');\n\nfunction briefItem(item: WorkItemRow) {\n return {\n id: item.id,\n number: workItemNumber(item),\n title: truncate(item.title, MAX_TEXT),\n stage: workItemStage(item),\n url:\n factoryWorkItemUrl({\n factoryProjectId: item.factoryProjectId,\n board: boardForWorkItem(item),\n workItemId: item.id,\n }) ?? null,\n };\n}\n\nfunction settled(result: FactoryDecisionActionResult) {\n if (!result.ok) throw new Error(result.message);\n const { decision } = result;\n return { decisionId: decision.id, status: decision.status, workItemId: decision.workItemId };\n}\n\n/** The work-item write and subscription tools for one user session's turn. */\nexport function createFactoryWorkItemTools(\n scope: FactoryToolScope,\n deps: FactoryWorkItemToolDependencies,\n): IntegrationTools {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const audit: FactoryAuditEmit = async input => {\n await deps.auditRecorder.record({\n orgId: scope.orgId,\n // Turns nobody signed in started are recorded as this session's agent, as their moves are.\n actorId: scope.interactive ? scope.userId : `agent:session:${scope.sessionId}`,\n actorType: scope.interactive ? 'human' : 'agent',\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n ...input,\n });\n };\n const requireItem = async (ref: string) => {\n const item = await findWorkItem(deps.workItems, scope, ref);\n if (!item) throw workItemNotFound(ref);\n return item;\n };\n /**\n * Check `slug` is a repository linked to this Factory and that a card with\n * `metadata` would run in it (a card synced from an issue follows the\n * issue's repository, which a slug can't override).\n */\n const requireRepository = async (slug: string, metadata: Record<string, unknown> | null = null) => {\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!sourceControl) throw new Error('This session has no source-control provider to look repositories up in.');\n const resolve = (repository?: string) =>\n resolveWorkItemRepository({\n sourceControl,\n ...project,\n item: { metadata: repository ? { ...metadata, repository } : metadata },\n });\n const resolution = await resolve(slug);\n if (resolution.status === 'resolved' && resolution.slug === slug) return;\n if (resolution.status === 'resolved') {\n throw new Error(`This card runs in ${resolution.slug}, which comes from its linked issue and can't be changed.`);\n }\n const linked = await resolve();\n const candidates =\n linked.status === 'ambiguous' ? linked.candidates : linked.status === 'resolved' ? [linked.slug] : [];\n throw new Error(\n candidates.length > 0\n ? `${slug} is not linked to this Factory. Linked repositories: ${candidates.join(', ')}.`\n : `${slug} is not linked to this Factory, and it has no linked repositories.`,\n );\n };\n const follow = (item: WorkItemRow, reason: 'created' | 'subscribed') =>\n deps.workItems.subscribeToWorkItem({\n ...project,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason,\n });\n\n return {\n factory_create_work_item: createTool({\n id: 'factory_create_work_item',\n description:\n \"File a new work item on this Factory's Work board for the person you are working with. It lands in the board's first phase, exactly like pressing New card, and this session follows it. Check factory_list_work_items for an existing item first. Name the repository when the Factory links more than one; runs refuse to start on a card that doesn't say which.\",\n inputSchema: z\n .object({\n title: z.string().trim().min(1).max(512).describe('Short title, as it should read on the card.'),\n brief: z\n .string()\n .trim()\n .min(1)\n .max(8000)\n .describe('What needs doing and why; posted as the first comment on the card.'),\n repository: repositorySchema.optional(),\n })\n .strict(),\n execute: async ({ title, brief, repository }) => {\n if (repository) await requireRepository(repository);\n const created = await createFactoryWorkItem({\n workItems: deps.workItems,\n boards: deps.boards,\n transitionService: deps.transitionService,\n audit,\n ...project,\n userId: scope.userId,\n ...(scope.actorProfile ? { origin: { actorProfile: scope.actorProfile } } : {}),\n input: { title, ...(repository ? { metadata: { repository } } : {}) },\n });\n if (created.status === 'refused') throw new Error(created.message);\n if (created.status === 'rejected') throw new Error(`The board refused the new item: ${created.reason}`);\n const { item } = created;\n // The card exists from here on: report follow-up failures alongside it\n // rather than failing the call, so the agent doesn't file it twice.\n const warnings: string[] = [];\n try {\n const comment = await deps.feed.createComment({\n orgId: scope.orgId,\n workItemId: item.id,\n author: { kind: 'user', id: scope.userId },\n body: brief,\n });\n if (comment.status !== 'created') {\n warnings.push(\n `The brief was not posted (${comment.status === 'invalid' ? comment.message : comment.status}).`,\n );\n }\n } catch (error) {\n warnings.push(`The brief was not posted: ${error instanceof Error ? error.message : String(error)}`);\n }\n try {\n await follow(item, 'created');\n } catch (error) {\n warnings.push(\n `This session does not follow it yet: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n return warnings.length ? { ...briefItem(item), warnings } : briefItem(item);\n },\n }),\n\n factory_transition_work_item: createTool({\n id: 'factory_transition_work_item',\n description:\n \"Move a work item to another stage of its board, as the person you are working with. The board's rules decide whether the move is allowed and what it starts.\",\n inputSchema: z\n .object({\n workItem: workItemRefSchema,\n stage: z.string().trim().min(1).max(64).describe('Destination stage, e.g. \"planning\" or \"done\".'),\n })\n .strict(),\n execute: async ({ workItem, stage }) => {\n if (!deps.transitionService) throw new Error('Factory stage transitions are not available right now.');\n const item = await requireItem(workItem);\n const parsed = transitionBodySchema.safeParse({\n board: boardForWorkItem(item),\n stage,\n expectedRevision: item.revision,\n requestId: randomUUID(),\n cause: 'factory_tools',\n });\n if (!parsed.success) throw new Error(`\"${stage}\" is not a valid stage name.`);\n const result = await deps.transitionService.transition({\n ...parsed.data,\n ...project,\n workItemId: item.id,\n ...(scope.interactive\n ? {\n actor: { type: 'human' as const, id: scope.userId },\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n ingress: { ...parsed.data.ingress, identity: `human:${scope.userId}:${parsed.data.ingress.identity}` },\n }\n : {\n // Nobody signed in started this turn: move as this session's agent, so the\n // plan gate and the agent's done gates apply and the move arms no runs.\n actor: { type: 'agent' as const, bindingId: `session:${scope.sessionId}`, role: 'session' },\n ingress: {\n type: 'agent' as const,\n identity: `session:${scope.sessionId}:${parsed.data.ingress.identity}`,\n },\n }),\n });\n if (result.status === 'rejected') throw new Error(`The move was refused (${result.code}): ${result.reason}`);\n return { workItemId: item.id, stage: result.stage, revision: result.revision };\n },\n }),\n\n factory_update_work_item: createTool({\n id: 'factory_update_work_item',\n description:\n \"Edit a work item's fields as the person you are working with: its title, the repository its runs use, its parent item, or pre-approving its plans. Move stages with factory_transition_work_item and start roles with factory_start_session; post to the card's feed for anything else.\",\n inputSchema: z\n .object({\n workItem: workItemRefSchema,\n title: z.string().trim().min(1).max(500).optional().describe('New card title.'),\n repository: repositorySchema.optional(),\n parentWorkItem: workItemRefSchema\n .nullable()\n .optional()\n .describe('Parent work item (id or #number), or null to detach it from its parent.'),\n plansPreapproved: z\n .literal(true)\n .optional()\n .describe(\n \"Pre-approve the item's plans, so its runs no longer stop for plan approval. Can't be undone here.\",\n ),\n })\n .strict()\n .refine(input => Object.keys(input).some(key => key !== 'workItem'), 'Pass at least one field to change.'),\n execute: async ({ workItem, title, repository, parentWorkItem, plansPreapproved }) => {\n // Pre-approval drops the plan-approval gate for good, so it is the person's call.\n if (plansPreapproved) requirePerson(scope, 'pre-approve plans');\n const item = await requireItem(workItem);\n if (repository) await requireRepository(repository, item.metadata);\n let parentWorkItemId: string | null | undefined;\n if (parentWorkItem === null) parentWorkItemId = null;\n else if (parentWorkItem !== undefined) parentWorkItemId = (await requireItem(parentWorkItem)).id;\n const patch: UpdateWorkItemInput = {\n ...(title !== undefined ? { title } : {}),\n ...(repository ? { metadata: { repository } } : {}),\n ...(parentWorkItemId !== undefined ? { parentWorkItemId } : {}),\n ...(plansPreapproved ? { plansPreapproved } : {}),\n };\n let updated: Awaited<ReturnType<WorkItemsStorage['update']>>;\n try {\n updated = await deps.workItems.update({ orgId: scope.orgId, id: item.id, userId: scope.userId, patch });\n } catch (error) {\n if (error instanceof WorkItemRelationError) throw new Error(`The parent was refused: ${error.message}`);\n throw error;\n }\n if (!updated) throw workItemNotFound(workItem);\n const fields = [\n ...(title !== undefined ? ['title'] : []),\n ...(repository ? ['metadata'] : []),\n ...(parentWorkItemId !== undefined ? ['parentWorkItemId'] : []),\n ...(plansPreapproved ? ['plansPreapproved'] : []),\n ];\n await audit({\n action: 'factory.work_item.updated',\n factoryProjectId: scope.factoryProjectId,\n targets: [{ type: 'work_item', id: updated.item.id, name: updated.item.title }],\n metadata: { fields, cause: 'factory_tools' },\n });\n const metadata = updated.item.metadata ?? {};\n return {\n workItem: briefItem(updated.item),\n updated: fields.map(field => (field === 'metadata' ? 'repository' : field)),\n repository: typeof metadata.repository === 'string' ? metadata.repository : null,\n parentWorkItemId: updated.item.parentWorkItemId,\n plansPreapproved: updated.item.plansPreapprovedAt !== null,\n };\n },\n }),\n\n factory_retry_decision: createTool({\n id: 'factory_retry_decision',\n description:\n 'Retry one failed Factory run once its cause is fixed. Refused when the failure cannot be fixed by retrying or the item has moved on.',\n inputSchema: z.object({ decisionId }).strict(),\n execute: async ({ decisionId }) =>\n settled(await retryFactoryDecision({ workItems: deps.workItems, boards: deps.boards, ...project, decisionId })),\n }),\n\n factory_dismiss_decision: createTool({\n id: 'factory_dismiss_decision',\n description: 'Dismiss one proposed Factory run that is no longer wanted.',\n inputSchema: z.object({ decisionId }).strict(),\n execute: async ({ decisionId }) => {\n requirePerson(scope, 'dismiss a proposed run');\n return settled(\n await dismissFactoryDecision({\n workItems: deps.workItems,\n audit,\n ...project,\n userId: scope.userId,\n decisionId,\n }),\n );\n },\n }),\n\n factory_resolve_proposal: createTool({\n id: 'factory_resolve_proposal',\n description:\n \"Approve or dismiss one proposed Factory run. Approving starts the run and lets the item's later runs start without asking.\",\n inputSchema: z.object({ decisionId, action: z.enum(['approve', 'dismiss']) }).strict(),\n execute: async ({ decisionId, action }) => {\n requirePerson(scope, 'approve or dismiss a proposed run');\n return settled(\n await resolveFactoryProposal({\n workItems: deps.workItems,\n audit,\n ...project,\n userId: scope.userId,\n decisionId,\n action,\n }),\n );\n },\n }),\n\n factory_subscribe_work_item: createTool({\n id: 'factory_subscribe_work_item',\n description:\n 'Follow a work item from this session, so you hear about its progress and its sessions report to you. Items you file or start are followed already.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await requireItem(workItem);\n const subscription = await follow(item, 'subscribed');\n return { workItem: briefItem(item), subscribed: true, reason: subscription.reason };\n },\n }),\n\n factory_unsubscribe_work_item: createTool({\n id: 'factory_unsubscribe_work_item',\n description: 'Stop following a work item from this session.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await requireItem(workItem);\n const removed = await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n return { workItem: briefItem(item), subscribed: false, wasSubscribed: removed };\n },\n }),\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/factory",
3
- "version": "0.25.0-alpha.6",
3
+ "version": "0.25.0-alpha.7",
4
4
  "description": "Mastra Software Factory module: the server core behind the Mastra Software Factory — storage domains, integrations, and surfaces for agent-powered software delivery",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -58,11 +58,11 @@
58
58
  "hono": "^4.13.13",
59
59
  "posthog-node": "^5.57.1",
60
60
  "zod": "^4.6.7",
61
- "@mastra/auth-studio": "1.3.11-alpha.1",
62
61
  "@mastra/auth-workos": "1.6.8-alpha.0",
63
- "@mastra/code-sdk": "1.16.0-alpha.6",
64
- "@mastra/core": "1.80.0-alpha.5",
62
+ "@mastra/auth-studio": "1.3.11-alpha.1",
63
+ "@mastra/code-sdk": "1.16.0-alpha.7",
65
64
  "@mastra/mcp": "2.6.0-alpha.2",
65
+ "@mastra/core": "1.80.0-alpha.6",
66
66
  "@mastra/platform-workspace": "1.10.0-alpha.2",
67
67
  "@mastra/slack": "1.8.0-alpha.2"
68
68
  },
@@ -72,13 +72,13 @@
72
72
  "tsdown": "0.23.1",
73
73
  "typescript": "^6.0.3",
74
74
  "vitest": "4.1.12",
75
- "@internal/lint": "0.0.145",
76
75
  "@internal/types-builder": "0.0.120",
76
+ "@mastra/deployer": "1.80.0-alpha.6",
77
77
  "@internal/workspace": "0.0.17",
78
- "@mastra/deployer": "1.80.0-alpha.5",
78
+ "@internal/lint": "0.0.145",
79
79
  "@mastra/libsql": "1.27.1-alpha.1",
80
- "@mastra/server": "1.80.0-alpha.5",
81
- "@mastra/pg": "1.33.0-alpha.3"
80
+ "@mastra/pg": "1.33.0-alpha.3",
81
+ "@mastra/server": "1.80.0-alpha.6"
82
82
  },
83
83
  "engines": {
84
84
  "node": ">=22.19.0"