@mastra/factory 0.17.3-alpha.6 → 0.18.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boards/review.d.ts +2 -0
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +16 -6
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/work-transition-policy.d.ts.map +1 -1
- package/dist/boards/work-transition-policy.js +10 -0
- package/dist/boards/work-transition-policy.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +14 -3
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/default-rules.d.ts +17 -1
- package/dist/integrations/github/default-rules.d.ts.map +1 -1
- package/dist/integrations/github/default-rules.js +30 -6
- package/dist/integrations/github/default-rules.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +48 -2
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/gitlab/api.d.ts.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/default-rules.d.ts.map +1 -1
- package/dist/integrations/gitlab/default-rules.js +1 -0
- package/dist/integrations/gitlab/default-rules.js.map +1 -1
- package/dist/integrations/gitlab/reconciler.d.ts.map +1 -1
- package/dist/integrations/gitlab/reconciler.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.d.ts.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/rules.d.ts.map +1 -1
- package/dist/integrations/gitlab/rules.js +66 -0
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/gitlab/version-control.d.ts.map +1 -1
- package/dist/integrations/gitlab/version-control.js.map +1 -1
- package/dist/integrations/incidentio/api.d.ts.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/intake.d.ts.map +1 -1
- package/dist/integrations/incidentio/intake.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.d.ts.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/slack/emoji-shortcodes.generated.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.d.ts.map +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +144 -12
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +3 -1
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +4 -2
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/routes/contracts.d.ts +23 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +21 -0
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/projects.d.ts +22 -2
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +101 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +11 -2
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/review-source-tool.d.ts +103 -0
- package/dist/rules/review-source-tool.d.ts.map +1 -0
- package/dist/rules/review-source-tool.js +109 -0
- package/dist/rules/review-source-tool.js.map +1 -0
- package/dist/rules/tools.d.ts +1 -0
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +107 -32
- package/dist/rules/tools.js.map +1 -1
- package/dist/sandbox/materialization.d.ts +5 -1
- package/dist/sandbox/materialization.d.ts.map +1 -1
- package/dist/sandbox/materialization.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +13 -1
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js.map +1 -1
- package/dist/session/factory-session.d.ts +6 -0
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +28 -9
- package/dist/session/factory-session.js.map +1 -1
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +16 -6
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +13 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +30 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/supervisor/session-messaging.d.ts +4 -1
- package/dist/supervisor/session-messaging.d.ts.map +1 -1
- package/dist/supervisor/session-messaging.js +8 -2
- package/dist/supervisor/session-messaging.js.map +1 -1
- package/dist/supervisor/write-tools.d.ts +2 -1
- package/dist/supervisor/write-tools.d.ts.map +1 -1
- package/dist/supervisor/write-tools.js +9 -2
- package/dist/supervisor/write-tools.js.map +1 -1
- package/dist/workspace.d.ts +7 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +53 -14
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-gitlab-rereview/SKILL.md +3 -2
- package/factory-skills/factory-gitlab-review/SKILL.md +19 -3
- package/factory-skills/factory-rereview/SKILL.md +37 -12
- package/factory-skills/factory-review/SKILL.md +37 -12
- package/package.json +7 -7
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { RequestContext } from '@mastra/core/request-context';
|
|
2
|
+
import type { IntegrationTools } from '../integrations/base.js';
|
|
3
|
+
import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
|
|
4
|
+
import type { FactorySessionSourceLookup } from './binding-context.js';
|
|
5
|
+
export interface ReviewSourceOutput {
|
|
6
|
+
/**
|
|
7
|
+
* Browser-facing deep-link back to the Factory session that produced this
|
|
8
|
+
* review — the only field a review body publishes. Everything else on this
|
|
9
|
+
* shape is an input to in-run cross-checks and the session handoff, never a
|
|
10
|
+
* published field, so the GitHub/GitLab audience never sees the upstream
|
|
11
|
+
* Linear/Jira slug through the review comment.
|
|
12
|
+
*/
|
|
13
|
+
sessionUrl: string;
|
|
14
|
+
/**
|
|
15
|
+
* PR/MR author recorded on the review card at intake — for GitHub this is
|
|
16
|
+
* `pullRequest.user.login`, for GitLab it is the GitLab username. The skill
|
|
17
|
+
* cross-checks it against `gh pr view --json author` (`.author.login`) or
|
|
18
|
+
* the equivalent MR fetch; a mismatch means the session is bound to a
|
|
19
|
+
* different PR/MR than the one being reviewed.
|
|
20
|
+
*/
|
|
21
|
+
triggeredBy: string | null;
|
|
22
|
+
/**
|
|
23
|
+
* The externalSource recorded on the review card itself — the PR/MR the
|
|
24
|
+
* session was intake-bound to. The skill cross-checks the reviewed PR/MR
|
|
25
|
+
* against this shape so a review can never quietly land on the wrong target.
|
|
26
|
+
*/
|
|
27
|
+
reviewTarget: {
|
|
28
|
+
integrationId: string;
|
|
29
|
+
type: string;
|
|
30
|
+
externalId: string;
|
|
31
|
+
url: string | null;
|
|
32
|
+
} | null;
|
|
33
|
+
/**
|
|
34
|
+
* Repository identity stamped on the review card at intake — the
|
|
35
|
+
* binding-side value for the null-`url` cross-check fallback. The skill must
|
|
36
|
+
* compare this against an identity resolved independently of the card (e.g.
|
|
37
|
+
* `gh api repos/<owner>/<repo> --jq .id` — the numeric REST id — for the
|
|
38
|
+
* session checkout's repository);
|
|
39
|
+
* comparing two values derived from the same checkout proves nothing.
|
|
40
|
+
* `null` when intake recorded no repository identity — in that case there
|
|
41
|
+
* is no verifiable bound repository and the fallback must not publish.
|
|
42
|
+
*/
|
|
43
|
+
boundRepository: {
|
|
44
|
+
provider: 'github';
|
|
45
|
+
repositoryId: number;
|
|
46
|
+
} | {
|
|
47
|
+
provider: 'gitlab';
|
|
48
|
+
host: string | null;
|
|
49
|
+
projectId: number;
|
|
50
|
+
} | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Normalize the operator-provided browser-facing UI origin (the
|
|
54
|
+
* `MASTRACODE_PUBLIC_URL` env var) into the `uiOrigin` the tool accepts.
|
|
55
|
+
*
|
|
56
|
+
* Blank counts as unset: `mastracode/web/.env.schema` ships
|
|
57
|
+
* `MASTRACODE_PUBLIC_URL=` as the operator-facing line, so an empty or
|
|
58
|
+
* whitespace-only value is the expected "not configured" state — it must map
|
|
59
|
+
* to `null` (tool omitted) rather than register the tool and publish a
|
|
60
|
+
* hostless `sessionUrl` into a public review body.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveReviewSourceUiOrigin(raw: string | undefined): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* `factory_review_source` — read the routing facts behind the current review
|
|
65
|
+
* session (session deep-link, PR/MR author, the review target) so the review
|
|
66
|
+
* agent can attribute its verdict to a concrete originating run and detect a
|
|
67
|
+
* mis-targeted review before it publishes.
|
|
68
|
+
*
|
|
69
|
+
* The tool exists to make cross-PR misattribution detectable both in-run and
|
|
70
|
+
* after the fact: a review lands on GitHub/GitLab with the Factory session URL
|
|
71
|
+
* that produced it, and the human who investigates a suspicious review can
|
|
72
|
+
* jump straight to the run.
|
|
73
|
+
*
|
|
74
|
+
* All fields are derived server-side from the bound work item — nothing enters
|
|
75
|
+
* from the agent's tool arguments — so the session URL always points at the
|
|
76
|
+
* caller's own session, never one it was asked to name.
|
|
77
|
+
*
|
|
78
|
+
* The tool is intentionally omitted (returns `{}`) whenever it cannot produce
|
|
79
|
+
* a complete, correct provenance shape: no resolvable session address, no
|
|
80
|
+
* active binding, a non-review-role binding, no bound work item, or no
|
|
81
|
+
* configured browser-facing UI origin. Skills treat tool-absent identically to
|
|
82
|
+
* tool-failed: stop-don't-publish.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createReviewSourceTool(options: {
|
|
85
|
+
requestContext: RequestContext;
|
|
86
|
+
storage: Pick<WorkItemsStorage, 'findActiveRunBindingByThread' | 'findActiveRunBinding' | 'get'>;
|
|
87
|
+
sessions?: FactorySessionSourceLookup;
|
|
88
|
+
/**
|
|
89
|
+
* Browser-facing UI origin, no trailing slash. In a separate-SPA deployment
|
|
90
|
+
* (API host distinct from UI host) this is the UI host — the origin the
|
|
91
|
+
* emitted session link opens in. `MastraFactoryConfig.publicUrl` is the API
|
|
92
|
+
* origin and must not be used here; the caller resolves the UI origin (see
|
|
93
|
+
* `factory.ts`) via the `MASTRACODE_PUBLIC_URL` env var.
|
|
94
|
+
*
|
|
95
|
+
* `null` means no browser-facing origin is configured. Match the Slack
|
|
96
|
+
* session-link surface (`integrations/slack/slack.ts:168-171`, `:809-812`),
|
|
97
|
+
* which omits the deep-link entirely rather than publishing an API/localhost
|
|
98
|
+
* URL into a public body: the tool is dropped from the toolset, and the
|
|
99
|
+
* skill's tool-not-available branch stops publication.
|
|
100
|
+
*/
|
|
101
|
+
uiOrigin: string | null;
|
|
102
|
+
}): Promise<IntegrationTools>;
|
|
103
|
+
//# sourceMappingURL=review-source-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-source-tool.d.ts","sourceRoot":"","sources":["../../src/rules/review-source-tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC3F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAGvE,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,YAAY,EAAE;QACZ,aAAa,EAAE,MAAM,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;KACpB,GAAG,IAAI,CAAC;IACT;;;;;;;;;OASG;IACH,eAAe,EACX;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GAC5C;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAC9D,IAAI,CAAC;CACV;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAElF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,cAAc,EAAE,cAAc,CAAC;IAC/B,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,8BAA8B,GAAG,sBAAsB,GAAG,KAAK,CAAC,CAAC;IACjG,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAyC5B"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { resolveFactorySessionAddress } from "./binding-context.js";
|
|
2
|
+
import { createTool } from "@mastra/core/tools";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/rules/review-source-tool.ts
|
|
5
|
+
/**
|
|
6
|
+
* Normalize the operator-provided browser-facing UI origin (the
|
|
7
|
+
* `MASTRACODE_PUBLIC_URL` env var) into the `uiOrigin` the tool accepts.
|
|
8
|
+
*
|
|
9
|
+
* Blank counts as unset: `mastracode/web/.env.schema` ships
|
|
10
|
+
* `MASTRACODE_PUBLIC_URL=` as the operator-facing line, so an empty or
|
|
11
|
+
* whitespace-only value is the expected "not configured" state — it must map
|
|
12
|
+
* to `null` (tool omitted) rather than register the tool and publish a
|
|
13
|
+
* hostless `sessionUrl` into a public review body.
|
|
14
|
+
*/
|
|
15
|
+
function resolveReviewSourceUiOrigin(raw) {
|
|
16
|
+
return raw?.trim().replace(/\/+$/, "") || null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `factory_review_source` — read the routing facts behind the current review
|
|
20
|
+
* session (session deep-link, PR/MR author, the review target) so the review
|
|
21
|
+
* agent can attribute its verdict to a concrete originating run and detect a
|
|
22
|
+
* mis-targeted review before it publishes.
|
|
23
|
+
*
|
|
24
|
+
* The tool exists to make cross-PR misattribution detectable both in-run and
|
|
25
|
+
* after the fact: a review lands on GitHub/GitLab with the Factory session URL
|
|
26
|
+
* that produced it, and the human who investigates a suspicious review can
|
|
27
|
+
* jump straight to the run.
|
|
28
|
+
*
|
|
29
|
+
* All fields are derived server-side from the bound work item — nothing enters
|
|
30
|
+
* from the agent's tool arguments — so the session URL always points at the
|
|
31
|
+
* caller's own session, never one it was asked to name.
|
|
32
|
+
*
|
|
33
|
+
* The tool is intentionally omitted (returns `{}`) whenever it cannot produce
|
|
34
|
+
* a complete, correct provenance shape: no resolvable session address, no
|
|
35
|
+
* active binding, a non-review-role binding, no bound work item, or no
|
|
36
|
+
* configured browser-facing UI origin. Skills treat tool-absent identically to
|
|
37
|
+
* tool-failed: stop-don't-publish.
|
|
38
|
+
*/
|
|
39
|
+
async function createReviewSourceTool(options) {
|
|
40
|
+
if (options.uiOrigin === null) return {};
|
|
41
|
+
const resolution = await resolveFactorySessionAddress({
|
|
42
|
+
requestContext: options.requestContext,
|
|
43
|
+
storage: options.storage,
|
|
44
|
+
sessions: options.sessions
|
|
45
|
+
});
|
|
46
|
+
if (!resolution) return {};
|
|
47
|
+
const binding = resolution.binding ?? await options.storage.findActiveRunBinding(resolution.address);
|
|
48
|
+
if (!binding || binding.role !== "review") return {};
|
|
49
|
+
const item = await options.storage.get({
|
|
50
|
+
orgId: binding.orgId,
|
|
51
|
+
id: binding.workItemId
|
|
52
|
+
});
|
|
53
|
+
if (!item) return {};
|
|
54
|
+
const output = {
|
|
55
|
+
sessionUrl: buildSessionUrl({
|
|
56
|
+
uiOrigin: options.uiOrigin,
|
|
57
|
+
factoryProjectId: binding.factoryProjectId,
|
|
58
|
+
sessionId: binding.sessionId,
|
|
59
|
+
threadId: binding.threadId
|
|
60
|
+
}),
|
|
61
|
+
triggeredBy: readPullRequestAuthor(item),
|
|
62
|
+
reviewTarget: readReviewTarget(item),
|
|
63
|
+
boundRepository: readBoundRepository(item)
|
|
64
|
+
};
|
|
65
|
+
return { factory_review_source: createTool({
|
|
66
|
+
id: "factory_review_source",
|
|
67
|
+
description: "Read the routing facts behind this review session: a deep-link back to the Factory session that produced the review, the PR/MR author recorded on the review card, and the review target (the PR/MR the session was intake-bound to). Publish the session URL in the required `Factory Session` block of every review body so a misattributed review can always be traced back to the run that produced it. Cross-check `triggeredBy` and `reviewTarget` against the PR/MR you fetched in Phase 1, using `boundRepository` as the intake-stamped repository identity when `reviewTarget.url` is null; do not publish the `triggeredBy`, `reviewTarget`, or `boundRepository` fields.",
|
|
68
|
+
inputSchema: z.object({}),
|
|
69
|
+
execute: async () => output
|
|
70
|
+
}) };
|
|
71
|
+
}
|
|
72
|
+
function buildSessionUrl(input) {
|
|
73
|
+
return `${input.uiOrigin.replace(/\/+$/, "")}/factories/${encodeURIComponent(input.factoryProjectId)}/workspaces/${encodeURIComponent(input.sessionId)}/threads/${encodeURIComponent(input.threadId)}`;
|
|
74
|
+
}
|
|
75
|
+
function readReviewTarget(item) {
|
|
76
|
+
const source = item.externalSource;
|
|
77
|
+
if (!source) return null;
|
|
78
|
+
return {
|
|
79
|
+
integrationId: source.integrationId,
|
|
80
|
+
type: source.type,
|
|
81
|
+
externalId: source.externalId,
|
|
82
|
+
url: typeof source.url === "string" && source.url ? source.url : null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function readBoundRepository(item) {
|
|
86
|
+
const metadata = item.metadata;
|
|
87
|
+
if (!metadata) return null;
|
|
88
|
+
if (typeof metadata.githubRepositoryId === "number") return {
|
|
89
|
+
provider: "github",
|
|
90
|
+
repositoryId: metadata.githubRepositoryId
|
|
91
|
+
};
|
|
92
|
+
if (typeof metadata.gitlabProjectId === "number") return {
|
|
93
|
+
provider: "gitlab",
|
|
94
|
+
host: typeof metadata.gitlabHost === "string" && metadata.gitlabHost ? metadata.gitlabHost : null,
|
|
95
|
+
projectId: metadata.gitlabProjectId
|
|
96
|
+
};
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function readPullRequestAuthor(item) {
|
|
100
|
+
const metadata = item.metadata;
|
|
101
|
+
if (!metadata) return null;
|
|
102
|
+
const author = metadata.author;
|
|
103
|
+
if (typeof author === "string" && author.trim()) return author.trim();
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { createReviewSourceTool, resolveReviewSourceUiOrigin };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=review-source-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-source-tool.js","names":[],"sources":["../../src/rules/review-source-tool.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationTools } from '../integrations/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactorySessionSourceLookup } from './binding-context.js';\nimport { resolveFactorySessionAddress } from './binding-context.js';\n\nexport interface ReviewSourceOutput {\n /**\n * Browser-facing deep-link back to the Factory session that produced this\n * review — the only field a review body publishes. Everything else on this\n * shape is an input to in-run cross-checks and the session handoff, never a\n * published field, so the GitHub/GitLab audience never sees the upstream\n * Linear/Jira slug through the review comment.\n */\n sessionUrl: string;\n /**\n * PR/MR author recorded on the review card at intake — for GitHub this is\n * `pullRequest.user.login`, for GitLab it is the GitLab username. The skill\n * cross-checks it against `gh pr view --json author` (`.author.login`) or\n * the equivalent MR fetch; a mismatch means the session is bound to a\n * different PR/MR than the one being reviewed.\n */\n triggeredBy: string | null;\n /**\n * The externalSource recorded on the review card itself — the PR/MR the\n * session was intake-bound to. The skill cross-checks the reviewed PR/MR\n * against this shape so a review can never quietly land on the wrong target.\n */\n reviewTarget: {\n integrationId: string;\n type: string;\n externalId: string;\n url: string | null;\n } | null;\n /**\n * Repository identity stamped on the review card at intake — the\n * binding-side value for the null-`url` cross-check fallback. The skill must\n * compare this against an identity resolved independently of the card (e.g.\n * `gh api repos/<owner>/<repo> --jq .id` — the numeric REST id — for the\n * session checkout's repository);\n * comparing two values derived from the same checkout proves nothing.\n * `null` when intake recorded no repository identity — in that case there\n * is no verifiable bound repository and the fallback must not publish.\n */\n boundRepository:\n | { provider: 'github'; repositoryId: number }\n | { provider: 'gitlab'; host: string | null; projectId: number }\n | null;\n}\n\n/**\n * Normalize the operator-provided browser-facing UI origin (the\n * `MASTRACODE_PUBLIC_URL` env var) into the `uiOrigin` the tool accepts.\n *\n * Blank counts as unset: `mastracode/web/.env.schema` ships\n * `MASTRACODE_PUBLIC_URL=` as the operator-facing line, so an empty or\n * whitespace-only value is the expected \"not configured\" state — it must map\n * to `null` (tool omitted) rather than register the tool and publish a\n * hostless `sessionUrl` into a public review body.\n */\nexport function resolveReviewSourceUiOrigin(raw: string | undefined): string | null {\n return raw?.trim().replace(/\\/+$/, '') || null;\n}\n\n/**\n * `factory_review_source` — read the routing facts behind the current review\n * session (session deep-link, PR/MR author, the review target) so the review\n * agent can attribute its verdict to a concrete originating run and detect a\n * mis-targeted review before it publishes.\n *\n * The tool exists to make cross-PR misattribution detectable both in-run and\n * after the fact: a review lands on GitHub/GitLab with the Factory session URL\n * that produced it, and the human who investigates a suspicious review can\n * jump straight to the run.\n *\n * All fields are derived server-side from the bound work item — nothing enters\n * from the agent's tool arguments — so the session URL always points at the\n * caller's own session, never one it was asked to name.\n *\n * The tool is intentionally omitted (returns `{}`) whenever it cannot produce\n * a complete, correct provenance shape: no resolvable session address, no\n * active binding, a non-review-role binding, no bound work item, or no\n * configured browser-facing UI origin. Skills treat tool-absent identically to\n * tool-failed: stop-don't-publish.\n */\nexport async function createReviewSourceTool(options: {\n requestContext: RequestContext;\n storage: Pick<WorkItemsStorage, 'findActiveRunBindingByThread' | 'findActiveRunBinding' | 'get'>;\n sessions?: FactorySessionSourceLookup;\n /**\n * Browser-facing UI origin, no trailing slash. In a separate-SPA deployment\n * (API host distinct from UI host) this is the UI host — the origin the\n * emitted session link opens in. `MastraFactoryConfig.publicUrl` is the API\n * origin and must not be used here; the caller resolves the UI origin (see\n * `factory.ts`) via the `MASTRACODE_PUBLIC_URL` env var.\n *\n * `null` means no browser-facing origin is configured. Match the Slack\n * session-link surface (`integrations/slack/slack.ts:168-171`, `:809-812`),\n * which omits the deep-link entirely rather than publishing an API/localhost\n * URL into a public body: the tool is dropped from the toolset, and the\n * skill's tool-not-available branch stops publication.\n */\n uiOrigin: string | null;\n}): Promise<IntegrationTools> {\n if (options.uiOrigin === null) return {};\n const resolution = await resolveFactorySessionAddress({\n requestContext: options.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n if (!resolution) return {};\n const binding = resolution.binding ?? (await options.storage.findActiveRunBinding(resolution.address));\n // The tool exists only for review-role sessions. Work/plan/triage runs never\n // publish reviews on GitHub, so exposing them the tool would just clutter\n // their prompt.\n if (!binding || binding.role !== 'review') return {};\n\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) return {};\n\n const sessionUrl = buildSessionUrl({\n uiOrigin: options.uiOrigin,\n factoryProjectId: binding.factoryProjectId,\n sessionId: binding.sessionId,\n threadId: binding.threadId,\n });\n const triggeredBy = readPullRequestAuthor(item);\n const reviewTarget = readReviewTarget(item);\n const boundRepository = readBoundRepository(item);\n // Every field is pre-resolved server-side and returned as a plain object.\n // The tool takes no arguments and does no further I/O, so a review-role\n // session either sees a complete provenance shape or the tool is absent —\n // the skill treats those branches identically (stop-don't-publish).\n const output: ReviewSourceOutput = { sessionUrl, triggeredBy, reviewTarget, boundRepository };\n\n return {\n factory_review_source: createTool({\n id: 'factory_review_source',\n description:\n 'Read the routing facts behind this review session: a deep-link back to the Factory session that produced the review, the PR/MR author recorded on the review card, and the review target (the PR/MR the session was intake-bound to). Publish the session URL in the required `Factory Session` block of every review body so a misattributed review can always be traced back to the run that produced it. Cross-check `triggeredBy` and `reviewTarget` against the PR/MR you fetched in Phase 1, using `boundRepository` as the intake-stamped repository identity when `reviewTarget.url` is null; do not publish the `triggeredBy`, `reviewTarget`, or `boundRepository` fields.',\n inputSchema: z.object({}),\n execute: async (): Promise<ReviewSourceOutput> => output,\n }),\n };\n}\n\nfunction buildSessionUrl(input: {\n uiOrigin: string;\n factoryProjectId: string;\n sessionId: string;\n threadId: string;\n}): string {\n const origin = input.uiOrigin.replace(/\\/+$/, '');\n return `${origin}/factories/${encodeURIComponent(input.factoryProjectId)}/workspaces/${encodeURIComponent(input.sessionId)}/threads/${encodeURIComponent(input.threadId)}`;\n}\n\nfunction readReviewTarget(item: WorkItemRow): ReviewSourceOutput['reviewTarget'] {\n const source = item.externalSource;\n if (!source) return null;\n return {\n integrationId: source.integrationId,\n type: source.type,\n externalId: source.externalId,\n url: typeof source.url === 'string' && source.url ? source.url : null,\n };\n}\n\nfunction readBoundRepository(item: WorkItemRow): ReviewSourceOutput['boundRepository'] {\n const metadata = item.metadata;\n if (!metadata) return null;\n // Intake stamps repository identity on every PR/MR card:\n // `github/default-rules.ts` writes `githubRepositoryId`, and\n // `gitlab/default-rules.ts` writes `gitlabHost` + `gitlabProjectId`.\n if (typeof metadata.githubRepositoryId === 'number') {\n return { provider: 'github', repositoryId: metadata.githubRepositoryId };\n }\n if (typeof metadata.gitlabProjectId === 'number') {\n return {\n provider: 'gitlab',\n host: typeof metadata.gitlabHost === 'string' && metadata.gitlabHost ? metadata.gitlabHost : null,\n projectId: metadata.gitlabProjectId,\n };\n }\n return null;\n}\n\nfunction readPullRequestAuthor(item: WorkItemRow): string | null {\n const metadata = item.metadata;\n if (!metadata) return null;\n const author = metadata.author;\n if (typeof author === 'string' && author.trim()) return author.trim();\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;AA+DA,SAAgB,4BAA4B,KAAwC;CAClF,OAAO,KAAK,KAAK,CAAC,CAAC,QAAQ,QAAQ,EAAE,KAAK;AAC5C;;;;;;;;;;;;;;;;;;;;;;AAuBA,eAAsB,uBAAuB,SAkBf;CAC5B,IAAI,QAAQ,aAAa,MAAM,OAAO,CAAC;CACvC,MAAM,aAAa,MAAM,6BAA6B;EACpD,gBAAgB,QAAQ;EACxB,SAAS,QAAQ;EACjB,UAAU,QAAQ;CACpB,CAAC;CACD,IAAI,CAAC,YAAY,OAAO,CAAC;CACzB,MAAM,UAAU,WAAW,WAAY,MAAM,QAAQ,QAAQ,qBAAqB,WAAW,OAAO;CAIpG,IAAI,CAAC,WAAW,QAAQ,SAAS,UAAU,OAAO,CAAC;CAEnD,MAAM,OAAO,MAAM,QAAQ,QAAQ,IAAI;EAAE,OAAO,QAAQ;EAAO,IAAI,QAAQ;CAAW,CAAC;CACvF,IAAI,CAAC,MAAM,OAAO,CAAC;CAenB,MAAM,SAA6B;EAAE,YAblB,gBAAgB;GACjC,UAAU,QAAQ;GAClB,kBAAkB,QAAQ;GAC1B,WAAW,QAAQ;GACnB,UAAU,QAAQ;EACpB,CAQ8C;EAAG,aAP7B,sBAAsB,IAOiB;EAAG,cANzC,iBAAiB,IAMmC;EAAG,iBALpD,oBAAoB,IAK8C;CAAE;CAE5F,OAAO,EACL,uBAAuB,WAAW;EAChC,IAAI;EACJ,aACE;EACF,aAAa,EAAE,OAAO,CAAC,CAAC;EACxB,SAAS,YAAyC;CACpD,CAAC,EACH;AACF;AAEA,SAAS,gBAAgB,OAKd;CAET,OAAO,GADQ,MAAM,SAAS,QAAQ,QAAQ,EAC/B,EAAE,aAAa,mBAAmB,MAAM,gBAAgB,EAAE,cAAc,mBAAmB,MAAM,SAAS,EAAE,WAAW,mBAAmB,MAAM,QAAQ;AACzK;AAEA,SAAS,iBAAiB,MAAuD;CAC/E,MAAM,SAAS,KAAK;CACpB,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO;EACL,eAAe,OAAO;EACtB,MAAM,OAAO;EACb,YAAY,OAAO;EACnB,KAAK,OAAO,OAAO,QAAQ,YAAY,OAAO,MAAM,OAAO,MAAM;CACnE;AACF;AAEA,SAAS,oBAAoB,MAA0D;CACrF,MAAM,WAAW,KAAK;CACtB,IAAI,CAAC,UAAU,OAAO;CAItB,IAAI,OAAO,SAAS,uBAAuB,UACzC,OAAO;EAAE,UAAU;EAAU,cAAc,SAAS;CAAmB;CAEzE,IAAI,OAAO,SAAS,oBAAoB,UACtC,OAAO;EACL,UAAU;EACV,MAAM,OAAO,SAAS,eAAe,YAAY,SAAS,aAAa,SAAS,aAAa;EAC7F,WAAW,SAAS;CACtB;CAEF,OAAO;AACT;AAEA,SAAS,sBAAsB,MAAkC;CAC/D,MAAM,WAAW,KAAK;CACtB,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,SAAS,SAAS;CACxB,IAAI,OAAO,WAAW,YAAY,OAAO,KAAK,GAAG,OAAO,OAAO,KAAK;CACpE,OAAO;AACT"}
|
package/dist/rules/tools.d.ts
CHANGED
|
@@ -11,4 +11,5 @@ export declare function createFactoryTransitionTools(options: {
|
|
|
11
11
|
sessions?: FactorySessionSourceLookup;
|
|
12
12
|
boards?: BoardRegistry;
|
|
13
13
|
}): Promise<IntegrationTools>;
|
|
14
|
+
export declare const REVIEW_VERDICTS: readonly ["approve", "request changes"];
|
|
14
15
|
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/rules/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAKnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/rules/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAKnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEvE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AA8BxE,wBAAsB,4BAA4B,CAAC,OAAO,EAAE;IAC1D,cAAc,EAAE,cAAc,CAAC;IAC/B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,iBAAiB,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,CAAC;IAChE,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqF5B;AAED,eAAO,MAAM,eAAe,yCAA0C,CAAC"}
|
package/dist/rules/tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import { BOARD_IDENTIFIER_RE } from "./validation.js";
|
|
|
2
2
|
import { boardForWorkItem, workItemPhaseSemantics } from "../boards/semantics.js";
|
|
3
3
|
import { FACTORY_TRIAGE_TYPES } from "./types.js";
|
|
4
4
|
import "../boards/index.js";
|
|
5
|
+
import { WorkItemUpdateConflictError } from "../storage/domains/work-items/base.js";
|
|
5
6
|
import { resolveFactorySessionAddress } from "./binding-context.js";
|
|
6
7
|
import { createTool } from "@mastra/core/tools";
|
|
7
8
|
import { z } from "zod";
|
|
@@ -42,47 +43,121 @@ async function createFactoryTransitionTools(options) {
|
|
|
42
43
|
if (!item) return {};
|
|
43
44
|
isTriage = boardForWorkItem(item) === "work";
|
|
44
45
|
}
|
|
45
|
-
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
return {
|
|
47
|
+
...availableBinding.role === "review" ? createReviewVerdictTool(options, availableBinding.workItemId) : {},
|
|
48
|
+
factory_transition_work_item: createTool({
|
|
49
|
+
id: "factory_transition_work_item",
|
|
50
|
+
description: isTriage ? "Report the triage classification and request a governed stage transition for the Factory work item exactly bound to this thread. Only bugs may request Planning autonomously; closure outcomes may request a terminal stage. Feature requests and other non-bug classifications that remain open must stay in their current Intake or Triage stage for maintainer approval." : "Request a governed stage transition for the Factory work item exactly bound to this thread. Use the current revision from the factory-phase signal and explain why the transition is appropriate.",
|
|
51
|
+
inputSchema: isTriage ? triageTransitionInputSchema : transitionInputSchema,
|
|
52
|
+
requireApproval: true,
|
|
53
|
+
execute: async ({ stage, expectedRevision, rationale, triageType: requestedTriageType }, execution) => {
|
|
54
|
+
const currentAddress = (await resolveFactorySessionAddress({
|
|
55
|
+
requestContext: execution.requestContext,
|
|
56
|
+
storage: options.storage,
|
|
57
|
+
sessions: options.sessions
|
|
58
|
+
}))?.address ?? null;
|
|
59
|
+
const toolCallId = execution.agent?.toolCallId;
|
|
60
|
+
if (!currentAddress || !toolCallId) throw new Error("Factory transitions require an authenticated bound agent tool call.");
|
|
61
|
+
const binding = await options.storage.findActiveRunBinding(currentAddress);
|
|
62
|
+
if (!binding || binding.workItemId !== availableBinding.workItemId) throw new Error("Factory agent binding is unavailable, revoked, or no longer matches this session.");
|
|
63
|
+
const item = await options.storage.get({
|
|
64
|
+
orgId: binding.orgId,
|
|
65
|
+
id: binding.workItemId
|
|
66
|
+
});
|
|
67
|
+
if (!item) throw new Error("Bound Factory work item not found.");
|
|
68
|
+
const board = boardForWorkItem(item);
|
|
69
|
+
const triageType = board === "work" && binding.role === "triage" ? requestedTriageType : void 0;
|
|
70
|
+
return await options.transitionService.transition({
|
|
71
|
+
orgId: binding.orgId,
|
|
72
|
+
factoryProjectId: binding.factoryProjectId,
|
|
73
|
+
workItemId: binding.workItemId,
|
|
74
|
+
board,
|
|
75
|
+
stage,
|
|
76
|
+
expectedRevision,
|
|
77
|
+
actor: {
|
|
78
|
+
type: "agent",
|
|
79
|
+
bindingId: binding.id,
|
|
80
|
+
role: binding.role
|
|
81
|
+
},
|
|
82
|
+
ingress: {
|
|
83
|
+
type: "agent",
|
|
84
|
+
identity: `${binding.id}:${toolCallId}`
|
|
85
|
+
},
|
|
86
|
+
cause: rationale,
|
|
87
|
+
...triageType ? { triageType } : {}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const REVIEW_VERDICTS = ["approve", "request changes"];
|
|
94
|
+
const reviewVerdictInputSchema = z.object({
|
|
95
|
+
verdict: z.enum(REVIEW_VERDICTS),
|
|
96
|
+
reviewedHeadSha: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i, "Use the full commit SHA of the reviewed head.")
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* A review pass ends by recording its verdict on the card, which stays in
|
|
100
|
+
* Reviewing: Done is reserved for the merge, and a verdict is not a lane. The
|
|
101
|
+
* next push re-reviews from the recorded verdict.
|
|
102
|
+
*/
|
|
103
|
+
function createReviewVerdictTool(options, boundWorkItemId) {
|
|
104
|
+
return { factory_record_review_verdict: createTool({
|
|
105
|
+
id: "factory_record_review_verdict",
|
|
106
|
+
description: "Record the published review verdict and the PR head SHA it covers on the Review card bound to this thread. The card stays in Reviewing; this ends the review pass.",
|
|
107
|
+
inputSchema: reviewVerdictInputSchema,
|
|
108
|
+
requireApproval: false,
|
|
109
|
+
execute: async ({ verdict, reviewedHeadSha }, execution) => {
|
|
110
|
+
const resolution = await resolveFactorySessionAddress({
|
|
52
111
|
requestContext: execution.requestContext,
|
|
53
112
|
storage: options.storage,
|
|
54
113
|
sessions: options.sessions
|
|
55
|
-
})
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
58
|
-
const binding = await options.storage.findActiveRunBinding(currentAddress);
|
|
59
|
-
if (!binding || binding.workItemId !== availableBinding.workItemId) throw new Error("Factory agent binding is unavailable, revoked, or no longer matches this session.");
|
|
114
|
+
});
|
|
115
|
+
const binding = resolution ? await options.storage.findActiveRunBinding(resolution.address) : null;
|
|
116
|
+
if (!binding || binding.workItemId !== boundWorkItemId || binding.role !== "review") throw new Error("Factory review binding is unavailable, revoked, or no longer matches this session.");
|
|
60
117
|
const item = await options.storage.get({
|
|
61
118
|
orgId: binding.orgId,
|
|
62
119
|
id: binding.workItemId
|
|
63
120
|
});
|
|
64
121
|
if (!item) throw new Error("Bound Factory work item not found.");
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
122
|
+
if (boardForWorkItem(item) !== "review" || item.stages[0] !== "review") throw new Error(`Only a card in Reviewing records a verdict; this one is in ${item.stages.join(", ")}.`);
|
|
123
|
+
const reviewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
124
|
+
try {
|
|
125
|
+
await options.storage.update({
|
|
126
|
+
orgId: binding.orgId,
|
|
127
|
+
id: item.id,
|
|
128
|
+
userId: `agent:${binding.id}`,
|
|
129
|
+
expectedRevision: item.revision,
|
|
130
|
+
patch: { metadata: {
|
|
131
|
+
...item.metadata ?? {},
|
|
132
|
+
reviewVerdict: verdict,
|
|
133
|
+
reviewedHeadSha: reviewedHeadSha.toLowerCase(),
|
|
134
|
+
reviewedAt
|
|
135
|
+
} }
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!(error instanceof WorkItemUpdateConflictError)) throw error;
|
|
139
|
+
throw new Error("The Review card changed while recording the verdict; call again to record it on the current card.");
|
|
140
|
+
}
|
|
141
|
+
const parent = item.parentWorkItemId ? await options.storage.get({
|
|
142
|
+
orgId: binding.orgId,
|
|
143
|
+
id: item.parentWorkItemId
|
|
144
|
+
}) : null;
|
|
145
|
+
if (parent && boardForWorkItem(parent) === "work") await options.storage.update({
|
|
68
146
|
orgId: binding.orgId,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
bindingId: binding.id,
|
|
77
|
-
role: binding.role
|
|
78
|
-
},
|
|
79
|
-
ingress: {
|
|
80
|
-
type: "agent",
|
|
81
|
-
identity: `${binding.id}:${toolCallId}`
|
|
82
|
-
},
|
|
83
|
-
cause: rationale,
|
|
84
|
-
...triageType ? { triageType } : {}
|
|
147
|
+
id: parent.id,
|
|
148
|
+
userId: `agent:${binding.id}`,
|
|
149
|
+
patch: { metadata: {
|
|
150
|
+
reviewVerdict: verdict,
|
|
151
|
+
reviewedHeadSha: reviewedHeadSha.toLowerCase(),
|
|
152
|
+
reviewedAt
|
|
153
|
+
} }
|
|
85
154
|
});
|
|
155
|
+
return {
|
|
156
|
+
status: "recorded",
|
|
157
|
+
verdict,
|
|
158
|
+
reviewedHeadSha,
|
|
159
|
+
reviewedAt
|
|
160
|
+
};
|
|
86
161
|
}
|
|
87
162
|
}) };
|
|
88
163
|
}
|
|
@@ -105,6 +180,6 @@ function createRetiredSessionTool(workItemId, stage) {
|
|
|
105
180
|
});
|
|
106
181
|
}
|
|
107
182
|
//#endregion
|
|
108
|
-
export { createFactoryTransitionTools };
|
|
183
|
+
export { REVIEW_VERDICTS, createFactoryTransitionTools };
|
|
109
184
|
|
|
110
185
|
//# sourceMappingURL=tools.js.map
|
package/dist/rules/tools.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.js","names":[],"sources":["../../src/rules/tools.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem, workItemPhaseSemantics } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactorySessionSourceLookup } from './binding-context.js';\nimport { resolveFactorySessionAddress } from './binding-context.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport { FACTORY_TRIAGE_TYPES } from './types.js';\nimport { BOARD_IDENTIFIER_RE, MAX_BOARD_IDENTIFIER_LENGTH } from './validation.js';\n\nconst MAX_RATIONALE_LENGTH = 1_000;\n\nconst transitionInputSchema = z\n .object({\n stage: z.string().max(MAX_BOARD_IDENTIFIER_LENGTH).regex(BOARD_IDENTIFIER_RE),\n expectedRevision: z.number().int().positive(),\n // Providers strip maxLength from the JSON schema and models can't count characters, so a\n // hard cap invites overshoot-retry loops at the end of every run. Accept and clamp instead.\n rationale: z\n .string()\n .trim()\n .min(1)\n .transform(value =>\n value.length <= MAX_RATIONALE_LENGTH ? value : `${value.slice(0, MAX_RATIONALE_LENGTH - 1)}…`,\n ),\n // Sessions are shared across role rotations, so a non-triage agent sees the triage\n // agent's earlier call (with triageType) in its history and copies the shape. Accept\n // the key here and drop it in execute rather than fail the whole call on it.\n triageType: z.enum(FACTORY_TRIAGE_TYPES).optional(),\n })\n .strict();\n\nconst triageTransitionInputSchema = transitionInputSchema.extend({\n triageType: z.enum(FACTORY_TRIAGE_TYPES),\n});\n\nexport async function createFactoryTransitionTools(options: {\n requestContext: RequestContext;\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n sessions?: FactorySessionSourceLookup;\n boards?: BoardRegistry;\n}): Promise<IntegrationTools> {\n const resolution = await resolveFactorySessionAddress({\n requestContext: options.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n if (!resolution) return {};\n const availableBinding = resolution.binding ?? (await options.storage.findActiveRunBinding(resolution.address));\n if (!availableBinding) {\n // No live binding for this session. A resumed session whose binding was\n // revoked when its work item reached a terminal stage was retired out from\n // under the model: rather than silently drop the tool — which leaves the\n // resumed run to rationalize a false blocked state — surface a tool that\n // fails with the concrete reason. Threads that were never bound (or whose\n // binding was rotated to a peer role, leaving the item still in flight) get\n // no tool as before.\n const retired = await options.storage.findRunBindingBySession(resolution.address);\n if (retired && retired.status === 'revoked' && options.boards) {\n const item = await options.storage.get({ orgId: retired.orgId, id: retired.workItemId });\n if (item && workItemPhaseSemantics(options.boards, item)?.kind === 'terminal') {\n return { factory_transition_work_item: createRetiredSessionTool(retired.workItemId, item.stages[0]) };\n }\n }\n return {};\n }\n let isTriage = false;\n if (availableBinding.role === 'triage') {\n const item = await options.storage.get({ orgId: availableBinding.orgId, id: availableBinding.workItemId });\n if (!item) return {};\n isTriage = boardForWorkItem(item) === 'work';\n }\n\n return {\n factory_transition_work_item: createTool({\n id: 'factory_transition_work_item',\n description: isTriage\n ? 'Report the triage classification and request a governed stage transition for the Factory work item exactly bound to this thread. Only bugs may request Planning autonomously; closure outcomes may request a terminal stage. Feature requests and other non-bug classifications that remain open must stay in their current Intake or Triage stage for maintainer approval.'\n : 'Request a governed stage transition for the Factory work item exactly bound to this thread. Use the current revision from the factory-phase signal and explain why the transition is appropriate.',\n inputSchema: isTriage ? triageTransitionInputSchema : transitionInputSchema,\n requireApproval: true,\n execute: async ({ stage, expectedRevision, rationale, triageType: requestedTriageType }, execution) => {\n const currentResolution = await resolveFactorySessionAddress({\n requestContext: execution.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n const currentAddress = currentResolution?.address ?? null;\n const toolCallId = execution.agent?.toolCallId;\n if (!currentAddress || !toolCallId) {\n throw new Error('Factory transitions require an authenticated bound agent tool call.');\n }\n const binding = await options.storage.findActiveRunBinding(currentAddress);\n // Authority is the work item this session is bound to, not the individual\n // binding row. Handing the next role its turn in an existing session\n // rotates the binding, and tools built for the previous role stay live\n // across that rotation; keying on row identity would strand the run that\n // the rotation exists to start. Re-pointing a session at a different item\n // is the hijack this guards against.\n if (!binding || binding.workItemId !== availableBinding.workItemId) {\n throw new Error('Factory agent binding is unavailable, revoked, or no longer matches this session.');\n }\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) throw new Error('Bound Factory work item not found.');\n const board = boardForWorkItem(item);\n // Only a Work triage binding may classify; other roles can echo the key from history.\n const triageType = board === 'work' && binding.role === 'triage' ? requestedTriageType : undefined;\n\n const result = await options.transitionService.transition({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n board,\n stage,\n expectedRevision,\n actor: { type: 'agent', bindingId: binding.id, role: binding.role },\n ingress: { type: 'agent', identity: `${binding.id}:${toolCallId}` },\n cause: rationale,\n ...(triageType ? { triageType } : {}),\n });\n\n return result;\n },\n }),\n };\n}\n\n/**\n * A retired session's transition tool: it exists only to fail loudly. The run's\n * binding was revoked when its work item settled at a terminal stage, so there\n * is nothing to transition. Registering it (instead of dropping the tool) stops\n * a resumed model from inventing a false \"blocked\" state when the affordance it\n * expects has silently vanished.\n */\nfunction createRetiredSessionTool(workItemId: string, stage: string | undefined): IntegrationTools[string] {\n return createTool({\n id: 'factory_transition_work_item',\n description:\n 'This Factory session has been retired: its work item already reached a terminal stage and the run binding was revoked. No further transitions can be requested from this session.',\n inputSchema: transitionInputSchema,\n requireApproval: false,\n execute: async () => {\n throw new Error(\n `This Factory session has been retired: work item ${workItemId} reached a terminal stage${\n stage ? ` (${stage})` : ''\n } and its run binding was revoked. No transition can be requested from this session.`,\n );\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tools.js","names":[],"sources":["../../src/rules/tools.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem, workItemPhaseSemantics } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { WorkItemUpdateConflictError } from '../storage/domains/work-items/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactorySessionSourceLookup } from './binding-context.js';\nimport { resolveFactorySessionAddress } from './binding-context.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport { FACTORY_TRIAGE_TYPES } from './types.js';\nimport { BOARD_IDENTIFIER_RE, MAX_BOARD_IDENTIFIER_LENGTH } from './validation.js';\n\nconst MAX_RATIONALE_LENGTH = 1_000;\n\nconst transitionInputSchema = z\n .object({\n stage: z.string().max(MAX_BOARD_IDENTIFIER_LENGTH).regex(BOARD_IDENTIFIER_RE),\n expectedRevision: z.number().int().positive(),\n // Providers strip maxLength from the JSON schema and models can't count characters, so a\n // hard cap invites overshoot-retry loops at the end of every run. Accept and clamp instead.\n rationale: z\n .string()\n .trim()\n .min(1)\n .transform(value =>\n value.length <= MAX_RATIONALE_LENGTH ? value : `${value.slice(0, MAX_RATIONALE_LENGTH - 1)}…`,\n ),\n // Sessions are shared across role rotations, so a non-triage agent sees the triage\n // agent's earlier call (with triageType) in its history and copies the shape. Accept\n // the key here and drop it in execute rather than fail the whole call on it.\n triageType: z.enum(FACTORY_TRIAGE_TYPES).optional(),\n })\n .strict();\n\nconst triageTransitionInputSchema = transitionInputSchema.extend({\n triageType: z.enum(FACTORY_TRIAGE_TYPES),\n});\n\nexport async function createFactoryTransitionTools(options: {\n requestContext: RequestContext;\n storage: WorkItemsStorage;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n sessions?: FactorySessionSourceLookup;\n boards?: BoardRegistry;\n}): Promise<IntegrationTools> {\n const resolution = await resolveFactorySessionAddress({\n requestContext: options.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n if (!resolution) return {};\n const availableBinding = resolution.binding ?? (await options.storage.findActiveRunBinding(resolution.address));\n if (!availableBinding) {\n // No live binding for this session. A resumed session whose binding was\n // revoked when its work item reached a terminal stage was retired out from\n // under the model: rather than silently drop the tool — which leaves the\n // resumed run to rationalize a false blocked state — surface a tool that\n // fails with the concrete reason. Threads that were never bound (or whose\n // binding was rotated to a peer role, leaving the item still in flight) get\n // no tool as before.\n const retired = await options.storage.findRunBindingBySession(resolution.address);\n if (retired && retired.status === 'revoked' && options.boards) {\n const item = await options.storage.get({ orgId: retired.orgId, id: retired.workItemId });\n if (item && workItemPhaseSemantics(options.boards, item)?.kind === 'terminal') {\n return { factory_transition_work_item: createRetiredSessionTool(retired.workItemId, item.stages[0]) };\n }\n }\n return {};\n }\n let isTriage = false;\n if (availableBinding.role === 'triage') {\n const item = await options.storage.get({ orgId: availableBinding.orgId, id: availableBinding.workItemId });\n if (!item) return {};\n isTriage = boardForWorkItem(item) === 'work';\n }\n\n return {\n ...(availableBinding.role === 'review' ? createReviewVerdictTool(options, availableBinding.workItemId) : {}),\n factory_transition_work_item: createTool({\n id: 'factory_transition_work_item',\n description: isTriage\n ? 'Report the triage classification and request a governed stage transition for the Factory work item exactly bound to this thread. Only bugs may request Planning autonomously; closure outcomes may request a terminal stage. Feature requests and other non-bug classifications that remain open must stay in their current Intake or Triage stage for maintainer approval.'\n : 'Request a governed stage transition for the Factory work item exactly bound to this thread. Use the current revision from the factory-phase signal and explain why the transition is appropriate.',\n inputSchema: isTriage ? triageTransitionInputSchema : transitionInputSchema,\n requireApproval: true,\n execute: async ({ stage, expectedRevision, rationale, triageType: requestedTriageType }, execution) => {\n const currentResolution = await resolveFactorySessionAddress({\n requestContext: execution.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n const currentAddress = currentResolution?.address ?? null;\n const toolCallId = execution.agent?.toolCallId;\n if (!currentAddress || !toolCallId) {\n throw new Error('Factory transitions require an authenticated bound agent tool call.');\n }\n const binding = await options.storage.findActiveRunBinding(currentAddress);\n // Authority is the work item this session is bound to, not the individual\n // binding row. Handing the next role its turn in an existing session\n // rotates the binding, and tools built for the previous role stay live\n // across that rotation; keying on row identity would strand the run that\n // the rotation exists to start. Re-pointing a session at a different item\n // is the hijack this guards against.\n if (!binding || binding.workItemId !== availableBinding.workItemId) {\n throw new Error('Factory agent binding is unavailable, revoked, or no longer matches this session.');\n }\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) throw new Error('Bound Factory work item not found.');\n const board = boardForWorkItem(item);\n // Only a Work triage binding may classify; other roles can echo the key from history.\n const triageType = board === 'work' && binding.role === 'triage' ? requestedTriageType : undefined;\n\n const result = await options.transitionService.transition({\n orgId: binding.orgId,\n factoryProjectId: binding.factoryProjectId,\n workItemId: binding.workItemId,\n board,\n stage,\n expectedRevision,\n actor: { type: 'agent', bindingId: binding.id, role: binding.role },\n ingress: { type: 'agent', identity: `${binding.id}:${toolCallId}` },\n cause: rationale,\n ...(triageType ? { triageType } : {}),\n });\n\n return result;\n },\n }),\n };\n}\n\nexport const REVIEW_VERDICTS = ['approve', 'request changes'] as const;\n\nconst reviewVerdictInputSchema = z.object({\n verdict: z.enum(REVIEW_VERDICTS),\n reviewedHeadSha: z\n .string()\n .regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i, 'Use the full commit SHA of the reviewed head.'),\n});\n\n/**\n * A review pass ends by recording its verdict on the card, which stays in\n * Reviewing: Done is reserved for the merge, and a verdict is not a lane. The\n * next push re-reviews from the recorded verdict.\n */\nfunction createReviewVerdictTool(\n options: { requestContext: RequestContext; storage: WorkItemsStorage; sessions?: FactorySessionSourceLookup },\n boundWorkItemId: string,\n): IntegrationTools {\n return {\n factory_record_review_verdict: createTool({\n id: 'factory_record_review_verdict',\n description:\n 'Record the published review verdict and the PR head SHA it covers on the Review card bound to this thread. The card stays in Reviewing; this ends the review pass.',\n inputSchema: reviewVerdictInputSchema,\n requireApproval: false,\n execute: async ({ verdict, reviewedHeadSha }, execution) => {\n const resolution = await resolveFactorySessionAddress({\n requestContext: execution.requestContext,\n storage: options.storage,\n sessions: options.sessions,\n });\n const binding = resolution ? await options.storage.findActiveRunBinding(resolution.address) : null;\n if (!binding || binding.workItemId !== boundWorkItemId || binding.role !== 'review') {\n throw new Error('Factory review binding is unavailable, revoked, or no longer matches this session.');\n }\n const item = await options.storage.get({ orgId: binding.orgId, id: binding.workItemId });\n if (!item) throw new Error('Bound Factory work item not found.');\n if (boardForWorkItem(item) !== 'review' || item.stages[0] !== 'review') {\n throw new Error(`Only a card in Reviewing records a verdict; this one is in ${item.stages.join(', ')}.`);\n }\n const reviewedAt = new Date().toISOString();\n // Revision-checked, so a merge that moves the card between the stage\n // check and this write fails the call instead of stamping a Done card.\n try {\n await options.storage.update({\n orgId: binding.orgId,\n id: item.id,\n userId: `agent:${binding.id}`,\n expectedRevision: item.revision,\n patch: {\n metadata: {\n ...(item.metadata ?? {}),\n reviewVerdict: verdict,\n reviewedHeadSha: reviewedHeadSha.toLowerCase(),\n reviewedAt,\n },\n },\n });\n } catch (error) {\n if (!(error instanceof WorkItemUpdateConflictError)) throw error;\n throw new Error(\n 'The Review card changed while recording the verdict; call again to record it on the current card.',\n );\n }\n // Mirror the verdict onto the Work item that authored the PR, so its\n // builder cannot close the work while changes are still requested.\n const parent = item.parentWorkItemId\n ? await options.storage.get({ orgId: binding.orgId, id: item.parentWorkItemId })\n : null;\n if (parent && boardForWorkItem(parent) === 'work') {\n await options.storage.update({\n orgId: binding.orgId,\n id: parent.id,\n userId: `agent:${binding.id}`,\n patch: {\n metadata: { reviewVerdict: verdict, reviewedHeadSha: reviewedHeadSha.toLowerCase(), reviewedAt },\n },\n });\n }\n return { status: 'recorded', verdict, reviewedHeadSha, reviewedAt };\n },\n }),\n };\n}\n\n/**\n * A retired session's transition tool: it exists only to fail loudly. The run's\n * binding was revoked when its work item settled at a terminal stage, so there\n * is nothing to transition. Registering it (instead of dropping the tool) stops\n * a resumed model from inventing a false \"blocked\" state when the affordance it\n * expects has silently vanished.\n */\nfunction createRetiredSessionTool(workItemId: string, stage: string | undefined): IntegrationTools[string] {\n return createTool({\n id: 'factory_transition_work_item',\n description:\n 'This Factory session has been retired: its work item already reached a terminal stage and the run binding was revoked. No further transitions can be requested from this session.',\n inputSchema: transitionInputSchema,\n requireApproval: false,\n execute: async () => {\n throw new Error(\n `This Factory session has been retired: work item ${workItemId} reached a terminal stage${\n stage ? ` (${stage})` : ''\n } and its run binding was revoked. No transition can be requested from this session.`,\n );\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -13,8 +13,12 @@ export interface SandboxCommandResult {
|
|
|
13
13
|
* so this type promotes it to required. `id` is taken from core; the command
|
|
14
14
|
* signature is spelled out because core's `CommandResult` is wider than what
|
|
15
15
|
* these helpers read (see below).
|
|
16
|
+
*
|
|
17
|
+
* `sandboxId` is the provider's physical, reattachable VM id (undefined until
|
|
18
|
+
* started, or for providers with no separate physical id); it is carried here
|
|
19
|
+
* so callers can persist it for deterministic reattach on resume.
|
|
16
20
|
*/
|
|
17
|
-
export type ExecutableSandbox = Pick<WorkspaceSandbox, 'id'> & {
|
|
21
|
+
export type ExecutableSandbox = Pick<WorkspaceSandbox, 'id' | 'sandboxId'> & {
|
|
18
22
|
/**
|
|
19
23
|
* Runs a command. The options are core's, but the result is only the part
|
|
20
24
|
* these helpers read: core's `CommandResult` also carries `success` and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materialization.d.ts","sourceRoot":"","sources":["../../src/sandbox/materialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEtF,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED
|
|
1
|
+
{"version":3,"file":"materialization.d.ts","sourceRoot":"","sources":["../../src/sandbox/materialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEtF,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,WAAW,CAAC,GAAG;IAC3E;;;;;OAKG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClH,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,iBAAiB,CAOxE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materialization.js","names":[],"sources":["../../src/sandbox/materialization.ts"],"sourcesContent":["import type { ExecuteCommandOptions, WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Result of one command executed inside a sandbox. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * A sandbox that can run commands.\n *\n * `executeCommand` is optional on `WorkspaceSandbox` because some providers\n * only offer a filesystem. Every git and materialization helper here needs it,\n * so this type promotes it to required. `id` is taken from core; the command\n * signature is spelled out because core's `CommandResult` is wider than what\n * these helpers read (see below).\n */\nexport type ExecutableSandbox = Pick<WorkspaceSandbox, 'id'> & {\n /**\n * Runs a command. The options are core's, but the result is only the part\n * these helpers read: core's `CommandResult` also carries `success` and\n * `executionTimeMs`, and asking for them would reject test doubles that\n * report exactly what the helpers consume.\n */\n executeCommand(command: string, args?: string[], options?: ExecuteCommandOptions): Promise<SandboxCommandResult>;\n};\n\n/**\n * Narrow a sandbox to one that can run commands, or fail saying which\n * capability is missing.\n *\n * Callers hold a `WorkspaceSandbox`, whose `executeCommand` is optional.\n * Optional-chaining past it would hand every caller `undefined` where it\n * expects an exit code, turning a misconfigured provider into a git command\n * that silently did nothing.\n */\nexport function requireExec(sandbox: WorkspaceSandbox): ExecutableSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not support executeCommand, which is required to run git and filesystem operations in a session.`,\n );\n }\n return sandbox as ExecutableSandbox;\n}\n"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"materialization.js","names":[],"sources":["../../src/sandbox/materialization.ts"],"sourcesContent":["import type { ExecuteCommandOptions, WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Result of one command executed inside a sandbox. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * A sandbox that can run commands.\n *\n * `executeCommand` is optional on `WorkspaceSandbox` because some providers\n * only offer a filesystem. Every git and materialization helper here needs it,\n * so this type promotes it to required. `id` is taken from core; the command\n * signature is spelled out because core's `CommandResult` is wider than what\n * these helpers read (see below).\n *\n * `sandboxId` is the provider's physical, reattachable VM id (undefined until\n * started, or for providers with no separate physical id); it is carried here\n * so callers can persist it for deterministic reattach on resume.\n */\nexport type ExecutableSandbox = Pick<WorkspaceSandbox, 'id' | 'sandboxId'> & {\n /**\n * Runs a command. The options are core's, but the result is only the part\n * these helpers read: core's `CommandResult` also carries `success` and\n * `executionTimeMs`, and asking for them would reject test doubles that\n * report exactly what the helpers consume.\n */\n executeCommand(command: string, args?: string[], options?: ExecuteCommandOptions): Promise<SandboxCommandResult>;\n};\n\n/**\n * Narrow a sandbox to one that can run commands, or fail saying which\n * capability is missing.\n *\n * Callers hold a `WorkspaceSandbox`, whose `executeCommand` is optional.\n * Optional-chaining past it would hand every caller `undefined` where it\n * expects an exit code, turning a misconfigured provider into a git command\n * that silently did nothing.\n */\nexport function requireExec(sandbox: WorkspaceSandbox): ExecutableSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not support executeCommand, which is required to run git and filesystem operations in a session.`,\n );\n }\n return sandbox as ExecutableSandbox;\n}\n"],"mappings":";;;;;;;;;;AAyCA,SAAgB,YAAY,SAA8C;CACxE,IAAI,OAAO,QAAQ,mBAAmB,YACpC,MAAM,IAAI,MACR,qBAAqB,QAAQ,SAAS,wGACxC;CAEF,OAAO;AACT"}
|
|
@@ -8,6 +8,15 @@ import type { RepositoryAccess } from '../capabilities/version-control.js';
|
|
|
8
8
|
export interface FactorySandboxContext {
|
|
9
9
|
/** Stable session id — the sandbox identity. */
|
|
10
10
|
sessionId: string;
|
|
11
|
+
/**
|
|
12
|
+
* The provider's physical sandbox id persisted from a prior start, when the
|
|
13
|
+
* session has been started before. Providers that reattach by physical id
|
|
14
|
+
* (e.g. Railway) MUST forward it to the sandbox constructor so resume
|
|
15
|
+
* reattaches the original VM instead of provisioning a replacement. E2B and
|
|
16
|
+
* Platform accept it as a deterministic-reattach optimization. Undefined on
|
|
17
|
+
* a session's first ever start.
|
|
18
|
+
*/
|
|
19
|
+
sandboxId?: string;
|
|
11
20
|
/** owner/name of the repository, when the session is repo-backed. */
|
|
12
21
|
repoFullName?: string;
|
|
13
22
|
/**
|
|
@@ -38,6 +47,9 @@ export interface FactorySandboxContext {
|
|
|
38
47
|
* `workingDirectory` at a per-session directory (e.g.
|
|
39
48
|
* `join(root, ctx.sessionId)`); the repo checks out as a subdirectory of it.
|
|
40
49
|
*
|
|
50
|
+
* Forward `ctx.sandboxId` to providers that reattach by physical id so resume
|
|
51
|
+
* reattaches the original VM instead of provisioning a replacement.
|
|
52
|
+
*
|
|
41
53
|
* Returns a `MastraSandbox`, not the bare `WorkspaceSandbox` interface:
|
|
42
54
|
* factory relies on the base class for the start lifecycle and the runtime
|
|
43
55
|
* env, so providers extend it rather than reimplementing the contract.
|
|
@@ -48,7 +60,7 @@ export interface FactorySandboxContext {
|
|
|
48
60
|
*
|
|
49
61
|
* @example
|
|
50
62
|
* ```typescript
|
|
51
|
-
* sandbox: ({ sessionId }) => new E2BSandbox({ id: sessionId })
|
|
63
|
+
* sandbox: ({ sessionId, sandboxId }) => new E2BSandbox({ id: sessionId, sandboxId })
|
|
52
64
|
* ```
|
|
53
65
|
*/
|
|
54
66
|
export type MastraFactorySandboxConfig = (ctx: FactorySandboxContext) => MastraSandbox;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-sandbox.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAI3E;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,mBAAmB,EAAE,CAAC,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,SAAS,CAAC;CACpE;AAED
|
|
1
|
+
{"version":3,"file":"session-sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-sandbox.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAI3E;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,mBAAmB,EAAE,CAAC,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,SAAS,CAAC;CACpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,GAAG,EAAE,qBAAqB,KAAK,aAAa,CAAC;AAEvF;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpH;;;;;;;;;GASG;AACH,UAAU,mBAAmB;IAC3B,OAAO,EAAE,gBAAgB,CAAC;IAC1B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,gBAAgB,GAChC,mBAAmB,CAQrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,EACzB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CASjB;AAmBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAErF;AAED,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAG3D;AAED,4DAA4D;AAC5D,wBAAgB,+BAA+B,IAAI,IAAI,CAGtD;AAcD,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEjF;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAEjF;AAqCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,eAAe,EACpB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,gBAAgB,CAyBlB"}
|