@mastra/factory 0.8.0-alpha.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +660 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +42 -4
  4. package/dist/factory.js.map +1 -1
  5. package/dist/integrations/base.d.ts +8 -0
  6. package/dist/integrations/base.d.ts.map +1 -1
  7. package/dist/integrations/github/app-identity.d.ts +43 -0
  8. package/dist/integrations/github/app-identity.d.ts.map +1 -0
  9. package/dist/integrations/github/app-identity.js +62 -0
  10. package/dist/integrations/github/app-identity.js.map +1 -0
  11. package/dist/integrations/github/integration.d.ts +9 -0
  12. package/dist/integrations/github/integration.d.ts.map +1 -1
  13. package/dist/integrations/github/integration.js +9 -1
  14. package/dist/integrations/github/integration.js.map +1 -1
  15. package/dist/integrations/github/reconcile-worker.d.ts +5 -0
  16. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  17. package/dist/integrations/github/reconcile-worker.js +7 -0
  18. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  19. package/dist/integrations/github/routes.d.ts.map +1 -1
  20. package/dist/integrations/github/routes.js +11 -6
  21. package/dist/integrations/github/routes.js.map +1 -1
  22. package/dist/integrations/github/rules.d.ts +8 -0
  23. package/dist/integrations/github/rules.d.ts.map +1 -1
  24. package/dist/integrations/github/rules.js +30 -8
  25. package/dist/integrations/github/rules.js.map +1 -1
  26. package/dist/integrations/github/sandbox.d.ts +39 -6
  27. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  28. package/dist/integrations/github/sandbox.js +48 -14
  29. package/dist/integrations/github/sandbox.js.map +1 -1
  30. package/dist/integrations/github/webhook.d.ts +39 -0
  31. package/dist/integrations/github/webhook.d.ts.map +1 -1
  32. package/dist/integrations/github/webhook.js +74 -13
  33. package/dist/integrations/github/webhook.js.map +1 -1
  34. package/dist/integrations/linear/integration.d.ts.map +1 -1
  35. package/dist/integrations/linear/integration.js +1 -0
  36. package/dist/integrations/linear/integration.js.map +1 -1
  37. package/dist/integrations/linear/routes.d.ts +10 -0
  38. package/dist/integrations/linear/routes.d.ts.map +1 -1
  39. package/dist/integrations/linear/routes.js +35 -1
  40. package/dist/integrations/linear/routes.js.map +1 -1
  41. package/dist/integrations/linear/rules.d.ts.map +1 -1
  42. package/dist/integrations/linear/rules.js +2 -1
  43. package/dist/integrations/linear/rules.js.map +1 -1
  44. package/dist/integrations/platform/github/event-worker.d.ts +11 -0
  45. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  46. package/dist/integrations/platform/github/event-worker.js +64 -16
  47. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  48. package/dist/integrations/platform/github/integration.d.ts +12 -0
  49. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  50. package/dist/integrations/platform/github/integration.js +47 -4
  51. package/dist/integrations/platform/github/integration.js.map +1 -1
  52. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  53. package/dist/integrations/platform/linear/integration.js +1 -0
  54. package/dist/integrations/platform/linear/integration.js.map +1 -1
  55. package/dist/integrations/slack/slack.d.ts.map +1 -1
  56. package/dist/integrations/slack/slack.js +2 -1
  57. package/dist/integrations/slack/slack.js.map +1 -1
  58. package/dist/routes/config.d.ts +12 -6
  59. package/dist/routes/config.d.ts.map +1 -1
  60. package/dist/routes/config.js +97 -56
  61. package/dist/routes/config.js.map +1 -1
  62. package/dist/routes/fs.d.ts.map +1 -1
  63. package/dist/routes/fs.js +2 -0
  64. package/dist/routes/fs.js.map +1 -1
  65. package/dist/routes/intake.d.ts +15 -0
  66. package/dist/routes/intake.d.ts.map +1 -1
  67. package/dist/routes/intake.js +65 -2
  68. package/dist/routes/intake.js.map +1 -1
  69. package/dist/routes/knowledge.d.ts +152 -0
  70. package/dist/routes/knowledge.d.ts.map +1 -0
  71. package/dist/routes/knowledge.js +442 -0
  72. package/dist/routes/knowledge.js.map +1 -0
  73. package/dist/routes/projects.d.ts +9 -1
  74. package/dist/routes/projects.d.ts.map +1 -1
  75. package/dist/routes/projects.js +8 -0
  76. package/dist/routes/projects.js.map +1 -1
  77. package/dist/routes/surface.d.ts +6 -0
  78. package/dist/routes/surface.d.ts.map +1 -1
  79. package/dist/routes/surface.js +10 -0
  80. package/dist/routes/surface.js.map +1 -1
  81. package/dist/routes/work-items.d.ts.map +1 -1
  82. package/dist/routes/work-items.js +3 -1
  83. package/dist/routes/work-items.js.map +1 -1
  84. package/dist/rules/binding-context.d.ts.map +1 -1
  85. package/dist/rules/binding-context.js +4 -1
  86. package/dist/rules/binding-context.js.map +1 -1
  87. package/dist/rules/defaults.d.ts.map +1 -1
  88. package/dist/rules/defaults.js +87 -4
  89. package/dist/rules/defaults.js.map +1 -1
  90. package/dist/rules/dispatcher.d.ts +6 -0
  91. package/dist/rules/dispatcher.d.ts.map +1 -1
  92. package/dist/rules/dispatcher.js +118 -16
  93. package/dist/rules/dispatcher.js.map +1 -1
  94. package/dist/rules/processor.d.ts.map +1 -1
  95. package/dist/rules/processor.js +20 -7
  96. package/dist/rules/processor.js.map +1 -1
  97. package/dist/rules/resolve.d.ts +1 -0
  98. package/dist/rules/resolve.d.ts.map +1 -1
  99. package/dist/rules/resolve.js +3 -2
  100. package/dist/rules/resolve.js.map +1 -1
  101. package/dist/rules/start-coordinator.d.ts +2 -0
  102. package/dist/rules/start-coordinator.d.ts.map +1 -1
  103. package/dist/rules/start-coordinator.js +3 -1
  104. package/dist/rules/start-coordinator.js.map +1 -1
  105. package/dist/rules/terminal-cleanup.d.ts +23 -0
  106. package/dist/rules/terminal-cleanup.d.ts.map +1 -0
  107. package/dist/rules/terminal-cleanup.js +43 -0
  108. package/dist/rules/terminal-cleanup.js.map +1 -0
  109. package/dist/rules/tools.d.ts.map +1 -1
  110. package/dist/rules/tools.js +24 -2
  111. package/dist/rules/tools.js.map +1 -1
  112. package/dist/rules/transition-service.d.ts +3 -0
  113. package/dist/rules/transition-service.d.ts.map +1 -1
  114. package/dist/rules/transition-service.js +10 -6
  115. package/dist/rules/transition-service.js.map +1 -1
  116. package/dist/rules/types.d.ts +32 -3
  117. package/dist/rules/types.d.ts.map +1 -1
  118. package/dist/rules/types.js +2 -0
  119. package/dist/rules/types.js.map +1 -1
  120. package/dist/rules/validation.d.ts.map +1 -1
  121. package/dist/rules/validation.js +8 -3
  122. package/dist/rules/validation.js.map +1 -1
  123. package/dist/sandbox/base-checkpoint-triggers.d.ts +77 -0
  124. package/dist/sandbox/base-checkpoint-triggers.d.ts.map +1 -0
  125. package/dist/sandbox/base-checkpoint-triggers.js +137 -0
  126. package/dist/sandbox/base-checkpoint-triggers.js.map +1 -0
  127. package/dist/sandbox/base-checkpoint.d.ts +59 -0
  128. package/dist/sandbox/base-checkpoint.d.ts.map +1 -0
  129. package/dist/sandbox/base-checkpoint.js +145 -0
  130. package/dist/sandbox/base-checkpoint.js.map +1 -0
  131. package/dist/sandbox/fleet.d.ts +32 -0
  132. package/dist/sandbox/fleet.d.ts.map +1 -1
  133. package/dist/sandbox/fleet.js +39 -10
  134. package/dist/sandbox/fleet.js.map +1 -1
  135. package/dist/session/checkpoint-capture.d.ts +3 -2
  136. package/dist/session/checkpoint-capture.d.ts.map +1 -1
  137. package/dist/session/checkpoint-capture.js +3 -3
  138. package/dist/session/checkpoint-capture.js.map +1 -1
  139. package/dist/session/factory-session.d.ts.map +1 -1
  140. package/dist/session/factory-session.js +2 -1
  141. package/dist/session/factory-session.js.map +1 -1
  142. package/dist/session/filesystem-capture.d.ts +7 -0
  143. package/dist/session/filesystem-capture.d.ts.map +1 -1
  144. package/dist/session/filesystem-capture.js +41 -4
  145. package/dist/session/filesystem-capture.js.map +1 -1
  146. package/dist/session/model-pack-hydration.d.ts +55 -0
  147. package/dist/session/model-pack-hydration.d.ts.map +1 -0
  148. package/dist/session/model-pack-hydration.js +56 -0
  149. package/dist/session/model-pack-hydration.js.map +1 -0
  150. package/dist/skills/service.d.ts +9 -0
  151. package/dist/skills/service.d.ts.map +1 -1
  152. package/dist/skills/service.js +10 -1
  153. package/dist/skills/service.js.map +1 -1
  154. package/dist/storage/domains/intake/base.d.ts +36 -0
  155. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  156. package/dist/storage/domains/intake/base.js +91 -2
  157. package/dist/storage/domains/intake/base.js.map +1 -1
  158. package/dist/storage/domains/model-packs/base.d.ts +26 -0
  159. package/dist/storage/domains/model-packs/base.d.ts.map +1 -1
  160. package/dist/storage/domains/model-packs/base.js +102 -13
  161. package/dist/storage/domains/model-packs/base.js.map +1 -1
  162. package/dist/storage/domains/source-control/base.d.ts +47 -1
  163. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  164. package/dist/storage/domains/source-control/base.js +78 -19
  165. package/dist/storage/domains/source-control/base.js.map +1 -1
  166. package/dist/storage/domains/source-control/inmemory.d.ts +11 -3
  167. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  168. package/dist/storage/domains/source-control/inmemory.js +10 -1
  169. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  170. package/dist/storage/domains/work-items/base.d.ts +67 -1
  171. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  172. package/dist/storage/domains/work-items/base.js +183 -12
  173. package/dist/storage/domains/work-items/base.js.map +1 -1
  174. package/dist/workspace.d.ts +31 -2
  175. package/dist/workspace.d.ts.map +1 -1
  176. package/dist/workspace.js +225 -54
  177. package/dist/workspace.js.map +1 -1
  178. package/factory-skills/factory-plan/SKILL.md +1 -1
  179. package/factory-skills/factory-rereview/SKILL.md +3 -3
  180. package/factory-skills/factory-review/SKILL.md +3 -3
  181. package/factory-skills/factory-triage/SKILL.md +2 -0
  182. package/package.json +8 -8
package/dist/workspace.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getFactoryAuthUserFromContext, getFactoryAuthUserId } from "./auth.js";
2
2
  import { getGithubPat } from "./integrations/github/pat.js";
3
3
  import { registerGithubPatKind, registerGithubTokenInjector } from "./integrations/github/token-refresh.js";
4
- import { DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, checkoutSessionBranch, materializeRepo, recycleClaimedWorkdir, runWorktreeSetup, runWorktreeTeardown } from "./integrations/github/sandbox.js";
4
+ import { DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, checkoutSessionBranch, hasExistingCheckout, materializeRepo, recycleClaimedWorkdir, runWorktreeSetup, runWorktreeTeardown } from "./integrations/github/sandbox.js";
5
+ import { baseCheckpointIsStale } from "./sandbox/base-checkpoint-triggers.js";
5
6
  import { getFactorySessionAddress } from "./rules/binding-context.js";
6
7
  import path, { dirname, join } from "path";
7
8
  import { LocalSandbox, LocalSkillSource, Workspace } from "@mastra/core/workspace";
@@ -17,6 +18,43 @@ const SESSION_CHECKPOINT_PREFIX = "mastracode-session";
17
18
  function checkpointNameForSession(sessionId) {
18
19
  return `${SESSION_CHECKPOINT_PREFIX}-${sessionId}`;
19
20
  }
21
+ /**
22
+ * Whether a command failure means the sandbox itself is gone (destroyed by
23
+ * idle GC or provider teardown) AND the command provably never started, so
24
+ * reviving the sandbox and replaying the command cannot run a side effect
25
+ * twice. Matched by error name so any provider's equivalent error classes
26
+ * participate without a package dependency.
27
+ *
28
+ * `SandboxExecTransportError` means both WebSocket attempts closed without an
29
+ * exit frame against a live sandbox. It only proves the command never started
30
+ * when the transport never opened (`opened: false` — the upgrade was refused
31
+ * outright). When the transport opened, the command may have run and mutated
32
+ * state before the result was lost, so replaying `git commit`, uploads, or
33
+ * arbitrary shell commands could execute the side effect twice; those errors
34
+ * surface to the caller instead.
35
+ */
36
+ function isDeadSandboxError(error) {
37
+ if (!(error instanceof Error)) return false;
38
+ if (error.name === "SandboxDestroyedError") return true;
39
+ if (error.name === "SandboxExecTransportError") return error.opened === false;
40
+ return /sandbox .*(destroyed|no longer exists|not found)/i.test(error.message);
41
+ }
42
+ /**
43
+ * The local-provider equivalent of {@link isDeadSandboxError}: a local sandbox
44
+ * is just a directory, so it "dies" when that directory is removed — which
45
+ * session retirement does while an in-flight run still holds the handle.
46
+ *
47
+ * Node surfaces a missing `cwd` as ENOENT against the binary it tried to spawn
48
+ * (`spawn /bin/sh ENOENT`), which is textually identical to the shell itself
49
+ * being absent, and is also what a genuinely missing command reports. Probing
50
+ * the working directory is what separates "the sandbox is gone" from "that
51
+ * command does not exist", so only the former triggers a rebuild.
52
+ */
53
+ function isMissingWorkdirError(error, workdir) {
54
+ if (!workdir) return false;
55
+ if (error?.code !== "ENOENT") return false;
56
+ return !existsSync(workdir);
57
+ }
20
58
  const bundleDirectory = dirname(fileURLToPath(import.meta.url));
21
59
  const bundledFactorySkillsPath = join(bundleDirectory, "factory-skills");
22
60
  const FACTORY_SKILLS_SOURCE_PATH = [
@@ -68,6 +106,47 @@ var FactorySkillSource = class {
68
106
  return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);
69
107
  }
70
108
  };
109
+ function skillSourceEnoent(skillPath) {
110
+ const error = /* @__PURE__ */ new Error(`ENOENT: no such file or directory, '${skillPath}'`);
111
+ error.code = "ENOENT";
112
+ return error;
113
+ }
114
+ /**
115
+ * Sandbox-backed skill fallback that stays inert until the session sandbox is
116
+ * actually materialized. Skill discovery runs on latency-sensitive paths (the
117
+ * Factory start coordinator resolves the kickoff skill before the start route
118
+ * responds); without this guard the first project-root read would hit the lazy
119
+ * sandbox handle and force full provisioning + repo materialization. While the
120
+ * sandbox is unmaterialized, project skill roots simply appear empty — bundled
121
+ * Factory skills resolve from local disk via `FactorySkillSource`. Once the
122
+ * sandbox exists, every call delegates straight through.
123
+ */
124
+ var UnmaterializedAwareSkillSource = class {
125
+ fallback;
126
+ isMaterialized;
127
+ constructor(fallback, isMaterialized) {
128
+ this.fallback = fallback;
129
+ this.isMaterialized = isMaterialized;
130
+ }
131
+ async exists(skillPath) {
132
+ return this.isMaterialized() ? this.fallback.exists(skillPath) : false;
133
+ }
134
+ async stat(skillPath) {
135
+ if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);
136
+ return this.fallback.stat(skillPath);
137
+ }
138
+ async readFile(skillPath) {
139
+ if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);
140
+ return this.fallback.readFile(skillPath);
141
+ }
142
+ async readdir(skillPath) {
143
+ return this.isMaterialized() ? this.fallback.readdir(skillPath) : [];
144
+ }
145
+ realpath(skillPath) {
146
+ if (!this.isMaterialized()) return Promise.resolve(skillPath);
147
+ return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);
148
+ }
149
+ };
71
150
  const factorySkillExtension = {
72
151
  id: "web-factory",
73
152
  paths: [FACTORY_SKILLS_MOUNT],
@@ -106,6 +185,8 @@ function createWorkspaceFactory(options = {}) {
106
185
  const githubTokenInjectors = /* @__PURE__ */ new Map();
107
186
  const githubTokenReconciliations = /* @__PURE__ */ new Map();
108
187
  const inflightMaterializations = /* @__PURE__ */ new Map();
188
+ const materializedSandboxes = /* @__PURE__ */ new Map();
189
+ const constructedWorkspaces = /* @__PURE__ */ new Map();
109
190
  return async ({ requestContext, mastra, skillExtension }) => {
110
191
  const effectiveSkillExtension = skillExtension ?? factorySkillExtension;
111
192
  const ctx = requestContext.get("controller");
@@ -121,7 +202,7 @@ function createWorkspaceFactory(options = {}) {
121
202
  const user = getFactoryAuthUserFromContext(requestContext);
122
203
  const userId = getFactoryAuthUserId(user);
123
204
  if (!user?.organizationId || !userId) throw new Error(`Factory session ${session.sessionId} was resolved without a caller identity`);
124
- if (user.organizationId !== session.orgId || userId !== session.userId) throw new Error(`Factory session ${session.sessionId} is not available to the current user`);
205
+ if (user.organizationId !== session.orgId || session.visibility === "private" && userId !== session.userId) throw new Error(`Factory session ${session.sessionId} is not available to the current user`);
125
206
  if (!sandboxConfig || !github || !fleet) throw new Error("GitHub and sandbox providers are required to create a Factory session workspace");
126
207
  const storage = github.sourceControlStorage;
127
208
  const projectRepository = await storage.projectRepositories.get({
@@ -129,14 +210,13 @@ function createWorkspaceFactory(options = {}) {
129
210
  id: session.projectRepositoryId
130
211
  });
131
212
  if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);
132
- const connection = await storage.connections.get({
213
+ const [connection, repository] = await Promise.all([storage.connections.get({
133
214
  orgId: session.orgId,
134
215
  id: projectRepository.connectionId
135
- });
136
- const repository = await storage.repositories.get({
216
+ }), storage.repositories.get({
137
217
  orgId: session.orgId,
138
218
  id: projectRepository.repositoryId
139
- });
219
+ })]);
140
220
  if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);
141
221
  if (!await storage.installations.get({
142
222
  orgId: session.orgId,
@@ -144,15 +224,20 @@ function createWorkspaceFactory(options = {}) {
144
224
  })) throw new Error(`GitHub installation ${connection.installationId} was not found`);
145
225
  const repoFullName = repository.slug;
146
226
  let workdir = isLocalSandbox ? fleet.computeLocalSessionWorkdir(repoFullName, session.id) : session.sandboxWorkdir ?? projectRepository.sandboxWorkdir;
147
- if (ctx && workdir && ctx.getState()?.projectPath !== workdir) await ctx.setState({
148
- projectPath: workdir,
149
- projectName: repoFullName
150
- });
227
+ let stateSeeded = false;
228
+ if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {
229
+ await ctx.setState({
230
+ projectPath: workdir,
231
+ projectName: repoFullName
232
+ });
233
+ stateSeeded = true;
234
+ }
151
235
  const binding = {
152
236
  get sandboxId() {
153
237
  return session.sandboxId;
154
238
  },
155
239
  checkpointName: checkpointNameForSession(session.id),
240
+ ...!session.materializedAt && projectRepository.baseCheckpoint && !baseCheckpointIsStale(projectRepository) ? { seedCheckpointName: projectRepository.baseCheckpoint.name } : {},
156
241
  setSandboxId: async (id) => {
157
242
  await storage.sessions.setSandbox({
158
243
  id: session.id,
@@ -246,7 +331,11 @@ function createWorkspaceFactory(options = {}) {
246
331
  await workspace.destroy();
247
332
  evicted = true;
248
333
  } catch {}
249
- if (evicted && githubTokenInjectors.get(workspaceId) === registered) githubTokenInjectors.delete(workspaceId);
334
+ if (evicted && githubTokenInjectors.get(workspaceId) === registered) {
335
+ githubTokenInjectors.delete(workspaceId);
336
+ materializedSandboxes.delete(workspaceId);
337
+ constructedWorkspaces.delete(workspaceId);
338
+ }
250
339
  }
251
340
  throw error;
252
341
  }
@@ -256,12 +345,17 @@ function createWorkspaceFactory(options = {}) {
256
345
  let existing;
257
346
  try {
258
347
  existing = mastra?.getWorkspaceById(workspaceId);
259
- existing?.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);
260
348
  } catch {
261
349
  existing = void 0;
262
350
  }
263
- if (existing) return reconcileRegisteredWorkspace(existing);
264
- const materialize = async () => {
351
+ existing ??= constructedWorkspaces.get(workspaceId);
352
+ if (existing) {
353
+ existing.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);
354
+ return reconcileRegisteredWorkspace(existing);
355
+ }
356
+ const retiredError = () => /* @__PURE__ */ new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);
357
+ const materializeSandbox = async () => {
358
+ if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) throw retiredError();
265
359
  let claimedPooledSandbox = false;
266
360
  if (!isLocalSandbox && !session.sandboxId) {
267
361
  const pooled = await storage.sandboxPool.claim({ projectRepositoryId: session.projectRepositoryId });
@@ -284,7 +378,7 @@ function createWorkspaceFactory(options = {}) {
284
378
  ...isLocalSandbox ? { workingDirectory: workdir } : {},
285
379
  actingUserId: userId
286
380
  });
287
- const runMaterialize = (target) => materializeRepo({
381
+ const runMaterialize = (target, skipPull) => materializeRepo({
288
382
  row: {
289
383
  id: session.id,
290
384
  sandboxWorkdir: workdir,
@@ -296,19 +390,21 @@ function createWorkspaceFactory(options = {}) {
296
390
  },
297
391
  sandbox: target,
298
392
  token,
299
- storage: storage.sessions
393
+ storage: storage.sessions,
394
+ skipPullOnExistingCheckout: skipPull
300
395
  });
301
396
  const isGitMissing = (error) => error instanceof MaterializeError && error.code === "git-missing";
302
397
  let sandbox = await ensureSandbox();
303
398
  if (claimedPooledSandbox) await recycleClaimedWorkdir(sandbox, workdir, repository.defaultBranch);
399
+ const seededFromBaseCheckpoint = !!binding.seedCheckpointName && !session.materializedAt && !claimedPooledSandbox && await hasExistingCheckout(sandbox, workdir, repoFullName);
304
400
  try {
305
- await runMaterialize(sandbox);
401
+ await runMaterialize(sandbox, seededFromBaseCheckpoint);
306
402
  } catch (error) {
307
403
  if (!isGitMissing(error)) throw error;
308
404
  await fleet.teardownSandbox(binding, sandbox);
309
405
  sandbox = await ensureSandbox();
310
406
  try {
311
- await runMaterialize(sandbox);
407
+ await runMaterialize(sandbox, false);
312
408
  } catch (retryError) {
313
409
  if (isGitMissing(retryError)) await fleet.teardownSandbox(binding, sandbox);
314
410
  throw retryError;
@@ -320,7 +416,7 @@ function createWorkspaceFactory(options = {}) {
320
416
  token,
321
417
  repoFullName
322
418
  });
323
- if (projectRepository.setupCommand) try {
419
+ if (projectRepository.setupCommand && !seededFromBaseCheckpoint) try {
324
420
  await runWorktreeSetup(sandbox, workdir, projectRepository.setupCommand);
325
421
  } catch (setupError) {
326
422
  if (projectRepository.teardownCommand) try {
@@ -346,47 +442,122 @@ function createWorkspaceFactory(options = {}) {
346
442
  generation: 0,
347
443
  tokenReplacementPending: false
348
444
  };
445
+ if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {
446
+ try {
447
+ await fleet.teardownSandbox(binding, sandbox);
448
+ } catch (teardownError) {
449
+ console.warn("[Mastra Factory] Sandbox teardown after mid-materialization retirement failed", {
450
+ orgId: session.orgId,
451
+ sessionId: session.sessionId,
452
+ error: teardownError instanceof Error ? teardownError.message.slice(-2e3) : String(teardownError)
453
+ });
454
+ }
455
+ throw retiredError();
456
+ }
349
457
  githubTokenInjectors.set(workspaceId, tokenRegistration);
350
458
  registerGithubTokenContext(tokenRegistration);
351
- const filesystem = new SandboxFilesystem({
352
- sandbox,
353
- workdir
354
- });
355
- const projectSkillPaths = [
356
- path.join(configDir, "skills"),
357
- ".claude/skills",
358
- ".agents/skills"
359
- ];
360
- const skillPaths = [...effectiveSkillExtension?.paths ?? [], ...projectSkillPaths];
361
- const workspace = new Workspace({
362
- id: workspaceId,
363
- name: "Mastra Code Factory Session Workspace",
364
- filesystem,
365
- sandbox,
366
- tools: MASTRACODE_WORKSPACE_TOOLS,
367
- skills: skillPaths,
368
- skillSource: effectiveSkillExtension?.createSource(filesystem, projectSkillPaths) ?? filesystem
369
- });
370
- mastra?.addWorkspace(workspace, workspaceId, { source: "mastra" });
371
- if (!await workspaceRegistry.register(session.sessionId, workspaceId, workspaceGeneration, async () => {
372
- githubTokenInjectors.delete(workspaceId);
373
- await mastra?.removeWorkspace?.(workspaceId);
374
- })) throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);
375
- return workspace;
459
+ return sandbox;
376
460
  };
377
- const inflight = inflightMaterializations.get(workspaceId);
378
- if (inflight) return reconcileRegisteredWorkspace(await inflight);
379
- const materialization = materialize();
380
- inflightMaterializations.set(workspaceId, materialization);
381
- try {
382
- return await materialization;
383
- } finally {
384
- inflightMaterializations.delete(workspaceId);
385
- }
461
+ const ensureMaterialized = async () => {
462
+ const ready = materializedSandboxes.get(workspaceId);
463
+ if (ready) return ready;
464
+ let inflight = inflightMaterializations.get(workspaceId);
465
+ if (!inflight) {
466
+ inflight = materializeSandbox();
467
+ inflightMaterializations.set(workspaceId, inflight);
468
+ inflight.then((sb) => {
469
+ materializedSandboxes.set(workspaceId, sb);
470
+ workspace.skills?.refresh().catch(() => {});
471
+ }, () => {});
472
+ }
473
+ try {
474
+ return await inflight;
475
+ } finally {
476
+ if (inflightMaterializations.get(workspaceId) === inflight) inflightMaterializations.delete(workspaceId);
477
+ }
478
+ };
479
+ const lazySandbox = {
480
+ get id() {
481
+ return materializedSandboxes.get(workspaceId)?.id ?? workspaceId;
482
+ },
483
+ name: "Factory Lazy Sandbox",
484
+ get provider() {
485
+ return fleet.provider;
486
+ },
487
+ get status() {
488
+ return materializedSandboxes.has(workspaceId) ? "ready" : "pending";
489
+ },
490
+ get supportsCheckpoints() {
491
+ return materializedSandboxes.get(workspaceId)?.supportsCheckpoints ?? false;
492
+ },
493
+ getInstructions() {
494
+ return materializedSandboxes.get(workspaceId)?.getInstructions?.() ?? fleet.getInstructions();
495
+ },
496
+ clone() {
497
+ throw new Error("The Factory session sandbox cannot be cloned from a lazy handle.");
498
+ },
499
+ async start() {},
500
+ async getInfo() {
501
+ return (await ensureMaterialized()).getInfo();
502
+ },
503
+ async executeCommand(command, args, options) {
504
+ const sandbox = await ensureMaterialized();
505
+ try {
506
+ return await sandbox.executeCommand(command, args, options);
507
+ } catch (error) {
508
+ if (!isDeadSandboxError(error) && !(isLocalSandbox && isMissingWorkdirError(error, workdir))) throw error;
509
+ if (materializedSandboxes.get(workspaceId) === sandbox) materializedSandboxes.delete(workspaceId);
510
+ return (await ensureMaterialized()).executeCommand(command, args, options);
511
+ }
512
+ },
513
+ setEnvironmentVariable(name, value) {
514
+ const sandbox = materializedSandboxes.get(workspaceId);
515
+ if (!sandbox?.setEnvironmentVariable) throw new Error("The Factory session sandbox is not materialized yet.");
516
+ sandbox.setEnvironmentVariable(name, value);
517
+ },
518
+ async snapshot() {
519
+ await materializedSandboxes.get(workspaceId)?.snapshot?.();
520
+ },
521
+ async stop() {
522
+ await materializedSandboxes.get(workspaceId)?.stop?.();
523
+ }
524
+ };
525
+ const filesystem = new SandboxFilesystem({
526
+ id: `sandbox-fs:${workspaceId}:${workdir}`,
527
+ sandbox: lazySandbox,
528
+ workdir
529
+ });
530
+ const projectSkillPaths = [
531
+ path.join(configDir, "skills"),
532
+ ".claude/skills",
533
+ ".agents/skills"
534
+ ];
535
+ const guardedSkillFallback = new UnmaterializedAwareSkillSource(filesystem, () => materializedSandboxes.has(workspaceId));
536
+ const workspace = new Workspace({
537
+ id: workspaceId,
538
+ name: "Mastra Code Factory Session Workspace",
539
+ filesystem,
540
+ sandbox: lazySandbox,
541
+ tools: MASTRACODE_WORKSPACE_TOOLS,
542
+ skills: [...effectiveSkillExtension?.paths ?? [], ...projectSkillPaths],
543
+ skillSource: effectiveSkillExtension?.createSource(guardedSkillFallback, projectSkillPaths) ?? guardedSkillFallback
544
+ });
545
+ mastra?.addWorkspace(workspace, workspaceId, { source: "mastra" });
546
+ constructedWorkspaces.set(workspaceId, workspace);
547
+ if (!await workspaceRegistry.register(session.sessionId, workspaceId, workspaceGeneration, async () => {
548
+ githubTokenInjectors.delete(workspaceId);
549
+ materializedSandboxes.delete(workspaceId);
550
+ constructedWorkspaces.delete(workspaceId);
551
+ await mastra?.removeWorkspace?.(workspaceId);
552
+ })) throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);
553
+ if (stateSeeded) ensureMaterialized().catch((error) => {
554
+ console.error(`[factory:workspace] background materialization for ${workspaceId} failed`, error);
555
+ });
556
+ return workspace;
386
557
  };
387
558
  }
388
559
  const getFactoryWorkspace = createWorkspaceFactory();
389
560
  //#endregion
390
- export { FACTORY_SKILLS_SOURCE_PATH, FACTORY_SKILL_NAMES, FactoryWorkspaceRegistry, checkpointNameForSession, createWorkspaceFactory, getFactoryWorkspace };
561
+ export { FACTORY_SKILLS_SOURCE_PATH, FACTORY_SKILL_NAMES, FactoryWorkspaceRegistry, checkpointNameForSession, createWorkspaceFactory, getFactoryWorkspace, isDeadSandboxError, isMissingWorkdirError };
391
562
 
392
563
  //# sourceMappingURL=workspace.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.js","names":["#factorySource","#fallbackSkillRoots","#isFactoryPath","#factoryPath","#entries","#generations"],"sources":["../src/workspace.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport path, { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { MASTRACODE_WORKSPACE_TOOLS } from '@mastra/code-sdk/agents/tool-availability';\nimport { getDynamicWorkspace } from '@mastra/code-sdk/agents/workspace';\nimport type { WorkspaceSkillExtension } from '@mastra/code-sdk/agents/workspace';\nimport { DEFAULT_CONFIG_DIR } from '@mastra/code-sdk/constants';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { LocalSandbox, LocalSkillSource, Workspace } from '@mastra/core/workspace';\nimport type { SkillSource, SkillSourceEntry, SkillSourceStat } from '@mastra/core/workspace';\nimport { getFactoryAuthUserFromContext, getFactoryAuthUserId } from './auth.js';\nimport type { MastraFactorySandboxConfig } from './factory.js';\nimport type { GithubIntegration } from './integrations/github/integration.js';\nimport { getGithubPat } from './integrations/github/pat.js';\nimport type { GithubPatKind } from './integrations/github/pat.js';\nimport {\n checkoutSessionBranch,\n DEFAULT_COMMAND_TIMEOUT_MS,\n MaterializeError,\n materializeRepo,\n recycleClaimedWorkdir,\n runWorktreeSetup,\n runWorktreeTeardown,\n} from './integrations/github/sandbox.js';\nimport { registerGithubPatKind, registerGithubTokenInjector } from './integrations/github/token-refresh.js';\nimport { getFactorySessionAddress } from './rules/binding-context.js';\nimport type { SandboxBindingStore, SandboxFleet } from './sandbox/fleet.js';\nimport type { WorkItemsStorage } from './storage/domains/work-items/base.js';\n\nconst WORKSPACE_ID_PREFIX = 'mfw';\nconst SESSION_CHECKPOINT_PREFIX = 'mastracode-session';\n\nexport function checkpointNameForSession(sessionId: string): string {\n return `${SESSION_CHECKPOINT_PREFIX}-${sessionId}`;\n}\n\nconst bundleDirectory = dirname(fileURLToPath(import.meta.url));\nconst bundledFactorySkillsPath = join(bundleDirectory, 'factory-skills');\nexport const FACTORY_SKILLS_SOURCE_PATH =\n [\n // Deploy bundle: the consumer copies `factory-skills/` next to the built\n // server module (e.g. via its public/ dir).\n bundledFactorySkillsPath,\n // Package layout: `dist/../factory-skills` (also `src/../factory-skills`\n // when running tests against sources).\n join(bundleDirectory, '..', 'factory-skills'),\n // Consumer repo running from its package root before a build.\n join(process.cwd(), 'src', 'mastra', 'public', 'factory-skills'),\n ].find(existsSync) ?? bundledFactorySkillsPath;\nconst FACTORY_SKILLS_MOUNT = path.resolve(path.parse(process.cwd()).root, '__mastracode_factory_skills__');\nexport const FACTORY_SKILL_NAMES = new Set([\n 'configure-factory-rules',\n 'factory-complete-issue',\n 'factory-plan',\n 'factory-rereview',\n 'factory-review',\n 'factory-triage',\n]);\n\nclass FactorySkillSource implements SkillSource {\n readonly #factorySource = new LocalSkillSource({ basePath: FACTORY_SKILLS_SOURCE_PATH });\n readonly #fallbackSkillRoots: Set<string>;\n\n constructor(\n readonly fallback: SkillSource,\n fallbackSkillRoots: string[],\n ) {\n this.#fallbackSkillRoots = new Set(fallbackSkillRoots.map(skillPath => path.normalize(skillPath)));\n }\n\n #isFactoryPath(skillPath: string): boolean {\n const normalized = path.normalize(skillPath);\n return normalized === FACTORY_SKILLS_MOUNT || normalized.startsWith(`${FACTORY_SKILLS_MOUNT}${path.sep}`);\n }\n\n #factoryPath(skillPath: string): string {\n return path.relative(FACTORY_SKILLS_MOUNT, path.normalize(skillPath));\n }\n\n exists(skillPath: string): Promise<boolean> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.exists(this.#factoryPath(skillPath))\n : this.fallback.exists(skillPath);\n }\n\n stat(skillPath: string): Promise<SkillSourceStat> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.stat(this.#factoryPath(skillPath))\n : this.fallback.stat(skillPath);\n }\n\n readFile(skillPath: string): Promise<string | Buffer> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.readFile(this.#factoryPath(skillPath))\n : this.fallback.readFile(skillPath);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n if (this.#isFactoryPath(skillPath)) {\n return this.#factorySource.readdir(this.#factoryPath(skillPath));\n }\n const entries = await this.fallback.readdir(skillPath);\n if (this.#fallbackSkillRoots.has(path.normalize(skillPath))) {\n return entries.filter(entry => !FACTORY_SKILL_NAMES.has(entry.name));\n }\n return entries;\n }\n\n realpath(skillPath: string): Promise<string> {\n if (this.#isFactoryPath(skillPath)) return Promise.resolve(path.normalize(skillPath));\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\nconst factorySkillExtension: WorkspaceSkillExtension = {\n id: 'web-factory',\n paths: [FACTORY_SKILLS_MOUNT],\n createSource: (fallback, fallbackSkillRoots) => new FactorySkillSource(fallback, fallbackSkillRoots),\n};\n\ntype DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];\n\nexport interface CreateWorkspaceFactoryOptions {\n /** Factory sandbox runtime config (template machine + workdir base). */\n sandbox?: MastraFactorySandboxConfig;\n /** GitHub integration used to resolve Factory sessions and mint repo tokens. */\n github?: GithubIntegration;\n /** Fleet the per-session sandboxes are provisioned/reattached through. */\n fleet?: SandboxFleet;\n /** Work-items storage used to resolve the session's run-binding role, so\n * review-board sessions get the reviewer PAT as `GH_TOKEN`. Optional —\n * without it every session uses the default (worker) PAT. */\n workItems?: Pick<WorkItemsStorage, 'findRunBindingBySession'>;\n /** Runtime workspace/token registrations invalidated when a session retires. */\n workspaceRegistry?: FactoryWorkspaceRegistry;\n}\n\ntype WorkspaceUnregister = () => Promise<void> | void;\n\n/** Tracks dynamic Factory workspaces by persisted session id for retirement. */\nexport class FactoryWorkspaceRegistry {\n readonly #entries = new Map<string, Map<string, WorkspaceUnregister>>();\n readonly #generations = new Map<string, number>();\n\n generation(sessionId: string): number {\n return this.#generations.get(sessionId) ?? 0;\n }\n\n async register(\n sessionId: string,\n workspaceId: string,\n generation: number,\n unregister: WorkspaceUnregister,\n ): Promise<boolean> {\n if (generation !== this.generation(sessionId)) {\n await unregister();\n return false;\n }\n const entries = this.#entries.get(sessionId) ?? new Map<string, WorkspaceUnregister>();\n entries.set(workspaceId, unregister);\n this.#entries.set(sessionId, entries);\n return true;\n }\n\n async invalidateSession(sessionId: string): Promise<void> {\n this.#generations.set(sessionId, this.generation(sessionId) + 1);\n const entries = this.#entries.get(sessionId);\n if (!entries) return;\n this.#entries.delete(sessionId);\n const results = await Promise.allSettled([...entries.values()].map(unregister => unregister()));\n const failure = results.find(result => result.status === 'rejected');\n if (failure?.status === 'rejected') throw failure.reason;\n }\n}\n\nexport function createWorkspaceFactory(options: CreateWorkspaceFactoryOptions = {}) {\n const { sandbox: sandboxConfig, github, fleet, workItems } = options;\n const workspaceRegistry = options.workspaceRegistry ?? new FactoryWorkspaceRegistry();\n const isLocalSandbox = sandboxConfig?.machine instanceof LocalSandbox;\n type GithubTokenRegistration = {\n inject: (token: string) => void;\n patKind: GithubPatKind;\n ghToken: string;\n generation: number;\n tokenReplacementPending: boolean;\n };\n const githubTokenInjectors = new Map<string, GithubTokenRegistration>();\n const githubTokenReconciliations = new Map<string, Promise<void>>();\n // Concurrent requests for the same session (thread list + activity polling +\n // chat) must not each provision a sandbox and clone the repository. The\n // first caller materializes; followers await the same promise.\n const inflightMaterializations = new Map<string, Promise<Workspace>>();\n\n return async ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => {\n const effectiveSkillExtension = skillExtension ?? factorySkillExtension;\n const ctx = requestContext.get('controller') as AgentControllerRequestContext<MastraCodeState> | undefined;\n const session =\n ctx?.resourceId && github ? await github.sourceControlStorage.sessions.getBySessionId(ctx.resourceId) : null;\n\n if (!session) {\n if (sandboxConfig && !isLocalSandbox) {\n // Chat-only session on a remote-sandbox deploy: there is no repository\n // to materialize, and the server host must never execute commands on a\n // shared deployment. Run the session without a workspace (chat works,\n // workspace tools are simply not registered) instead of erroring on\n // every message.\n return undefined;\n }\n return getDynamicWorkspace({ requestContext, mastra, skillExtension: effectiveSkillExtension });\n }\n\n const user = getFactoryAuthUserFromContext(requestContext);\n const userId = getFactoryAuthUserId(user);\n // No identity at all is a server-side caller that forgot to seed one\n // (webhook, cron), not someone reaching for another user's session.\n if (!user?.organizationId || !userId) {\n throw new Error(`Factory session ${session.sessionId} was resolved without a caller identity`);\n }\n if (user.organizationId !== session.orgId || userId !== session.userId) {\n throw new Error(`Factory session ${session.sessionId} is not available to the current user`);\n }\n if (!sandboxConfig || !github || !fleet) {\n throw new Error('GitHub and sandbox providers are required to create a Factory session workspace');\n }\n\n const storage = github.sourceControlStorage;\n const projectRepository = await storage.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);\n const connection = await storage.connections.get({ orgId: session.orgId, id: projectRepository.connectionId });\n const repository = await storage.repositories.get({ orgId: session.orgId, id: projectRepository.repositoryId });\n if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);\n const installation = await storage.installations.get({ orgId: session.orgId, id: connection.installationId });\n if (!installation) throw new Error(`GitHub installation ${connection.installationId} was not found`);\n const repoFullName = repository.slug;\n\n let workdir = isLocalSandbox\n ? fleet.computeLocalSessionWorkdir(repoFullName, session.id)\n : (session.sandboxWorkdir ?? projectRepository.sandboxWorkdir);\n // The system prompt derives its working directory from `state.projectPath`\n // and falls back to the server's own process.cwd() when unset — which\n // points the agent at the host checkout (and lets it run `git checkout`\n // there instead of in its session workdir). Pin it to the session workdir.\n // During createSession this seeds the session's initial state (the\n // workspace resolves before the session is built); on later requests it\n // self-heals live state.\n if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {\n await ctx.setState({ projectPath: workdir, projectName: repoFullName });\n }\n const binding: SandboxBindingStore = {\n // Read through to the session row so teardown after a fresh provision\n // sees the just-persisted id instead of a stale snapshot.\n get sandboxId() {\n return session.sandboxId;\n },\n checkpointName: checkpointNameForSession(session.id),\n setSandboxId: async id => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: id, sandboxWorkdir: workdir });\n session.sandboxId = id;\n session.sandboxWorkdir = workdir;\n },\n clear: async () => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: null, sandboxWorkdir: workdir });\n session.sandboxId = null;\n },\n };\n\n const extensionId = effectiveSkillExtension ? `-${effectiveSkillExtension.id}` : '';\n const workspaceId = `${WORKSPACE_ID_PREFIX}-${projectRepository.id}-${session.id}${extensionId}`;\n const workspaceGeneration = workspaceRegistry.generation(session.sessionId);\n const configDir = sandboxConfig.workdir ?? DEFAULT_CONFIG_DIR;\n\n const getRepositoryToken = async (): Promise<string> => {\n const access = await github.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n return token;\n };\n const resolveGithubPatKind = async (fallback: GithubPatKind): Promise<GithubPatKind> => {\n if (!workItems) return 'default';\n try {\n const address = getFactorySessionAddress(requestContext);\n const runBinding = address ? await workItems.findRunBindingBySession(address) : null;\n return runBinding?.role === 'review' && runBinding.status === 'active' && runBinding.orgId === session.orgId\n ? 'reviewer'\n : 'default';\n } catch {\n // Preserve the installed role when binding storage is temporarily unavailable.\n return fallback;\n }\n };\n const registerGithubTokenContext = (registered: GithubTokenRegistration): void => {\n const generation = registered.generation;\n registerGithubTokenInjector(requestContext, token => {\n if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) {\n throw new Error('GitHub token refresh no longer matches the active Factory workspace role.');\n }\n registered.inject(token);\n });\n registerGithubPatKind(requestContext, registered.patKind);\n };\n const reconcileGithubToken = async (): Promise<void> => {\n const previous = githubTokenReconciliations.get(workspaceId) ?? Promise.resolve();\n const reconciliation = previous\n .catch(() => {})\n .then(async () => {\n const registered = githubTokenInjectors.get(workspaceId);\n if (!registered) return;\n\n const previousPatKind = registered.patKind;\n const patKind = await resolveGithubPatKind(previousPatKind);\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (patKind !== previousPatKind) {\n registered.patKind = patKind;\n registered.generation += 1;\n }\n if (patKind === 'reviewer') registered.tokenReplacementPending = false;\n if (previousPatKind === 'reviewer' && patKind === 'default') {\n // Invalidate reviewer refresh contexts before replacement I/O so\n // they cannot restore reviewer credentials after a failed downgrade.\n registered.tokenReplacementPending = true;\n }\n\n let token = await getGithubPat(() => github.integrationStorage, session.orgId, patKind);\n if (!token && registered.tokenReplacementPending) token = await getRepositoryToken();\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (token && token !== registered.ghToken) {\n try {\n registered.inject(token);\n } catch (error) {\n if (registered.tokenReplacementPending) throw error;\n // Same-role rotations and reviewer upgrades remain best-effort.\n }\n }\n if (token && token === registered.ghToken) registered.tokenReplacementPending = false;\n registerGithubTokenContext(registered);\n });\n githubTokenReconciliations.set(workspaceId, reconciliation);\n try {\n await reconciliation;\n } finally {\n if (githubTokenReconciliations.get(workspaceId) === reconciliation) {\n githubTokenReconciliations.delete(workspaceId);\n }\n }\n };\n const reconcileRegisteredWorkspace = async (workspace: Workspace): Promise<Workspace> => {\n const registered = githubTokenInjectors.get(workspaceId);\n try {\n await reconcileGithubToken();\n } catch (error) {\n if (registered?.tokenReplacementPending && githubTokenInjectors.get(workspaceId) === registered) {\n // The role generation already invalidated reviewer refresh contexts.\n // Keep the pending registration so failed eviction cannot make a\n // still-live reviewer workspace look safe on the next reuse.\n let evicted = false;\n try {\n evicted = (await mastra?.removeWorkspace?.(workspaceId)) === true;\n } catch {\n // Preserve the credential-replacement error and retry on the next reuse.\n }\n try {\n await workspace.destroy();\n evicted = true;\n } catch {\n // The pending registration keeps the workspace quarantined if cleanup also fails.\n }\n if (evicted && githubTokenInjectors.get(workspaceId) === registered) {\n githubTokenInjectors.delete(workspaceId);\n }\n }\n throw error;\n }\n if (registered && githubTokenInjectors.get(workspaceId) !== registered) {\n throw new Error('Factory workspace GitHub credential registration is no longer active.');\n }\n return workspace;\n };\n\n let existing: Workspace | undefined;\n try {\n existing = mastra?.getWorkspaceById(workspaceId) as Workspace | undefined;\n existing?.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);\n } catch {\n // Not registered yet.\n existing = undefined;\n }\n if (existing) {\n return reconcileRegisteredWorkspace(existing);\n }\n\n const materialize = async (): Promise<Workspace> => {\n // A terminal work item or a deleted session may have returned a\n // still-warm VM — with this repository already cloned — to the reuse\n // pool. Adopt it before provisioning a fresh sandbox. Pooled VMs carry\n // no credentials (tokens are injected per command, and the workdir is\n // scrubbed on release and again below), so any user's session for this\n // repository can claim one.\n let claimedPooledSandbox = false;\n if (!isLocalSandbox && !session.sandboxId) {\n const pooled = await storage.sandboxPool.claim({\n projectRepositoryId: session.projectRepositoryId,\n });\n if (pooled) {\n await storage.sessions.setSandbox({\n id: session.id,\n sandboxId: pooled.sandboxId,\n sandboxWorkdir: pooled.sandboxWorkdir,\n });\n session.sandboxId = pooled.sandboxId;\n session.sandboxWorkdir = pooled.sandboxWorkdir;\n workdir = pooled.sandboxWorkdir;\n claimedPooledSandbox = true;\n }\n }\n\n const token = await getRepositoryToken();\n\n // The `gh` CLI needs a PAT when the org configured one (installation\n // tokens 403 on integration-restricted endpoints); git clone/checkout\n // below keep using the minted installation token. Review-board sessions\n // (run-binding role `review`) authenticate `gh` as the reviewer account\n // when a reviewer token is configured; everything else — including\n // sessions with no resolvable run binding — uses the worker token.\n const patKind = await resolveGithubPatKind('default');\n const ghCliToken = (await getGithubPat(() => github.integrationStorage, session.orgId, patKind)) ?? token;\n\n const ensureSandbox = () =>\n fleet.ensureSandbox(binding, { GH_TOKEN: ghCliToken }, undefined, {\n ...(isLocalSandbox ? { workingDirectory: workdir } : {}),\n actingUserId: userId,\n });\n const runMaterialize = (target: Awaited<ReturnType<typeof ensureSandbox>>) =>\n materializeRepo({\n row: { id: session.id, sandboxWorkdir: workdir, materializedAt: session.materializedAt },\n repoInfo: { repoFullName: repoFullName, defaultBranch: repository.defaultBranch },\n sandbox: target,\n token,\n storage: storage.sessions,\n });\n const isGitMissing = (error: unknown) => error instanceof MaterializeError && error.code === 'git-missing';\n\n let sandbox = await ensureSandbox();\n // A claimed VM still has the previous session's branch checked out —\n // reset it to the default branch before materialize/checkout. When the\n // pooled VM was already reaped, `ensureSandbox` provisioned fresh and\n // the recycle is a no-op (no checkout on disk yet).\n if (claimedPooledSandbox) await recycleClaimedWorkdir(sandbox, workdir, repository.defaultBranch);\n try {\n await runMaterialize(sandbox);\n } catch (error) {\n if (!isGitMissing(error)) throw error;\n // A sandbox without git was booted from a bare base image (e.g. the\n // platform proxy falls back to a clean Debian base when its template\n // build fails). That VM can never materialize a repo, and its id is\n // already persisted on the binding — tear it down so re-opens stop\n // reattaching to the poisoned sandbox, then retry once on a fresh VM.\n await fleet.teardownSandbox(binding, sandbox);\n sandbox = await ensureSandbox();\n try {\n await runMaterialize(sandbox);\n } catch (retryError) {\n // Still bare — the provider's template is persistently broken.\n // Clear the binding so a later manual retry provisions fresh.\n if (isGitMissing(retryError)) await fleet.teardownSandbox(binding, sandbox);\n throw retryError;\n }\n }\n await checkoutSessionBranch(sandbox, workdir, {\n branch: session.branch,\n baseBranch: session.baseBranch || projectRepository.branch || repository.defaultBranch,\n token,\n repoFullName: repoFullName,\n });\n if (projectRepository.setupCommand) {\n try {\n await runWorktreeSetup(sandbox, workdir, projectRepository.setupCommand);\n } catch (setupError) {\n if (projectRepository.teardownCommand) {\n try {\n await runWorktreeTeardown(sandbox, workdir, projectRepository.teardownCommand, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (teardownError) {\n console.warn('[Mastra Factory] Worktree teardown after setup failure failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: teardownError instanceof Error ? teardownError.message.slice(-2000) : String(teardownError),\n });\n }\n }\n throw setupError;\n }\n }\n\n const tokenRegistration: GithubTokenRegistration = {\n inject: freshToken => {\n if (!sandbox.setEnvironmentVariable) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n sandbox.setEnvironmentVariable('GH_TOKEN', freshToken);\n tokenRegistration.ghToken = freshToken;\n },\n patKind,\n ghToken: ghCliToken,\n generation: 0,\n tokenReplacementPending: false,\n };\n githubTokenInjectors.set(workspaceId, tokenRegistration);\n registerGithubTokenContext(tokenRegistration);\n\n const filesystem = new SandboxFilesystem({ sandbox, workdir });\n const projectSkillPaths = [path.join(configDir, 'skills'), '.claude/skills', '.agents/skills'];\n const skillPaths = [...(effectiveSkillExtension?.paths ?? []), ...projectSkillPaths];\n const workspace = new Workspace({\n id: workspaceId,\n name: 'Mastra Code Factory Session Workspace',\n filesystem,\n sandbox: sandbox as unknown as ConstructorParameters<typeof Workspace>[0]['sandbox'],\n tools: MASTRACODE_WORKSPACE_TOOLS,\n skills: skillPaths,\n skillSource: effectiveSkillExtension?.createSource(filesystem, projectSkillPaths) ?? filesystem,\n });\n // Register with the Mastra instance so sync HTTP handlers that resolve\n // the workspace via `mastra.getWorkspaceById(id)` (file tree, permissions\n // probe, MCP/tool routes) find it instead of throwing\n // `MASTRA_GET_WORKSPACE_BY_ID_NOT_FOUND`. `addWorkspace` is idempotent on\n // key collision, so the inflight coalescing and reuse paths above stay\n // race-safe. Registration happens synchronously with the return so a\n // concurrent lookup on another request cannot observe an unregistered\n // workspace.\n mastra?.addWorkspace(workspace, workspaceId, { source: 'mastra' });\n const registered = await workspaceRegistry.register(\n session.sessionId,\n workspaceId,\n workspaceGeneration,\n async () => {\n githubTokenInjectors.delete(workspaceId);\n await mastra?.removeWorkspace?.(workspaceId);\n },\n );\n if (!registered) {\n throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);\n }\n return workspace;\n };\n\n // Dedupe concurrent materializations of the same workspace: followers\n // await the leader's promise instead of provisioning a second sandbox,\n // then bind the shared token injector into their own request context.\n const inflight = inflightMaterializations.get(workspaceId);\n if (inflight) {\n const workspace = await inflight;\n return reconcileRegisteredWorkspace(workspace);\n }\n const materialization = materialize();\n inflightMaterializations.set(workspaceId, materialization);\n try {\n return await materialization;\n } finally {\n inflightMaterializations.delete(workspaceId);\n }\n };\n}\n\nexport const getFactoryWorkspace = createWorkspaceFactory();\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace.js","names":["#factorySource","#fallbackSkillRoots","#isFactoryPath","#factoryPath","#entries","#generations"],"sources":["../src/workspace.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport path, { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { MASTRACODE_WORKSPACE_TOOLS } from '@mastra/code-sdk/agents/tool-availability';\nimport { getDynamicWorkspace } from '@mastra/code-sdk/agents/workspace';\nimport type { WorkspaceSkillExtension } from '@mastra/code-sdk/agents/workspace';\nimport { DEFAULT_CONFIG_DIR } from '@mastra/code-sdk/constants';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { LocalSandbox, LocalSkillSource, Workspace } from '@mastra/core/workspace';\nimport type { SkillSource, SkillSourceEntry, SkillSourceStat } from '@mastra/core/workspace';\nimport { getFactoryAuthUserFromContext, getFactoryAuthUserId } from './auth.js';\nimport type { MastraFactorySandboxConfig } from './factory.js';\nimport type { GithubIntegration } from './integrations/github/integration.js';\nimport { getGithubPat } from './integrations/github/pat.js';\nimport type { GithubPatKind } from './integrations/github/pat.js';\nimport {\n checkoutSessionBranch,\n hasExistingCheckout,\n DEFAULT_COMMAND_TIMEOUT_MS,\n MaterializeError,\n materializeRepo,\n recycleClaimedWorkdir,\n runWorktreeSetup,\n runWorktreeTeardown,\n} from './integrations/github/sandbox.js';\nimport { registerGithubPatKind, registerGithubTokenInjector } from './integrations/github/token-refresh.js';\nimport { getFactorySessionAddress } from './rules/binding-context.js';\nimport { baseCheckpointIsStale } from './sandbox/base-checkpoint-triggers.js';\nimport type { SandboxBindingStore, SandboxFleet } from './sandbox/fleet.js';\nimport type { WorkItemsStorage } from './storage/domains/work-items/base.js';\n\nconst WORKSPACE_ID_PREFIX = 'mfw';\nconst SESSION_CHECKPOINT_PREFIX = 'mastracode-session';\n\nexport function checkpointNameForSession(sessionId: string): string {\n return `${SESSION_CHECKPOINT_PREFIX}-${sessionId}`;\n}\n\n/**\n * Whether a command failure means the sandbox itself is gone (destroyed by\n * idle GC or provider teardown) AND the command provably never started, so\n * reviving the sandbox and replaying the command cannot run a side effect\n * twice. Matched by error name so any provider's equivalent error classes\n * participate without a package dependency.\n *\n * `SandboxExecTransportError` means both WebSocket attempts closed without an\n * exit frame against a live sandbox. It only proves the command never started\n * when the transport never opened (`opened: false` — the upgrade was refused\n * outright). When the transport opened, the command may have run and mutated\n * state before the result was lost, so replaying `git commit`, uploads, or\n * arbitrary shell commands could execute the side effect twice; those errors\n * surface to the caller instead.\n */\nexport function isDeadSandboxError(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n if (error.name === 'SandboxDestroyedError') return true;\n if (error.name === 'SandboxExecTransportError') {\n return (error as Error & { opened?: boolean }).opened === false;\n }\n return /sandbox .*(destroyed|no longer exists|not found)/i.test(error.message);\n}\n\n/**\n * The local-provider equivalent of {@link isDeadSandboxError}: a local sandbox\n * is just a directory, so it \"dies\" when that directory is removed — which\n * session retirement does while an in-flight run still holds the handle.\n *\n * Node surfaces a missing `cwd` as ENOENT against the binary it tried to spawn\n * (`spawn /bin/sh ENOENT`), which is textually identical to the shell itself\n * being absent, and is also what a genuinely missing command reports. Probing\n * the working directory is what separates \"the sandbox is gone\" from \"that\n * command does not exist\", so only the former triggers a rebuild.\n */\nexport function isMissingWorkdirError(error: unknown, workdir: string | undefined): boolean {\n if (!workdir) return false;\n if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') return false;\n return !existsSync(workdir);\n}\n\nconst bundleDirectory = dirname(fileURLToPath(import.meta.url));\nconst bundledFactorySkillsPath = join(bundleDirectory, 'factory-skills');\nexport const FACTORY_SKILLS_SOURCE_PATH =\n [\n // Deploy bundle: the consumer copies `factory-skills/` next to the built\n // server module (e.g. via its public/ dir).\n bundledFactorySkillsPath,\n // Package layout: `dist/../factory-skills` (also `src/../factory-skills`\n // when running tests against sources).\n join(bundleDirectory, '..', 'factory-skills'),\n // Consumer repo running from its package root before a build.\n join(process.cwd(), 'src', 'mastra', 'public', 'factory-skills'),\n ].find(existsSync) ?? bundledFactorySkillsPath;\nconst FACTORY_SKILLS_MOUNT = path.resolve(path.parse(process.cwd()).root, '__mastracode_factory_skills__');\nexport const FACTORY_SKILL_NAMES = new Set([\n 'configure-factory-rules',\n 'factory-complete-issue',\n 'factory-plan',\n 'factory-rereview',\n 'factory-review',\n 'factory-triage',\n]);\n\nclass FactorySkillSource implements SkillSource {\n readonly #factorySource = new LocalSkillSource({ basePath: FACTORY_SKILLS_SOURCE_PATH });\n readonly #fallbackSkillRoots: Set<string>;\n\n constructor(\n readonly fallback: SkillSource,\n fallbackSkillRoots: string[],\n ) {\n this.#fallbackSkillRoots = new Set(fallbackSkillRoots.map(skillPath => path.normalize(skillPath)));\n }\n\n #isFactoryPath(skillPath: string): boolean {\n const normalized = path.normalize(skillPath);\n return normalized === FACTORY_SKILLS_MOUNT || normalized.startsWith(`${FACTORY_SKILLS_MOUNT}${path.sep}`);\n }\n\n #factoryPath(skillPath: string): string {\n return path.relative(FACTORY_SKILLS_MOUNT, path.normalize(skillPath));\n }\n\n exists(skillPath: string): Promise<boolean> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.exists(this.#factoryPath(skillPath))\n : this.fallback.exists(skillPath);\n }\n\n stat(skillPath: string): Promise<SkillSourceStat> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.stat(this.#factoryPath(skillPath))\n : this.fallback.stat(skillPath);\n }\n\n readFile(skillPath: string): Promise<string | Buffer> {\n return this.#isFactoryPath(skillPath)\n ? this.#factorySource.readFile(this.#factoryPath(skillPath))\n : this.fallback.readFile(skillPath);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n if (this.#isFactoryPath(skillPath)) {\n return this.#factorySource.readdir(this.#factoryPath(skillPath));\n }\n const entries = await this.fallback.readdir(skillPath);\n if (this.#fallbackSkillRoots.has(path.normalize(skillPath))) {\n return entries.filter(entry => !FACTORY_SKILL_NAMES.has(entry.name));\n }\n return entries;\n }\n\n realpath(skillPath: string): Promise<string> {\n if (this.#isFactoryPath(skillPath)) return Promise.resolve(path.normalize(skillPath));\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\nfunction skillSourceEnoent(skillPath: string): Error {\n const error = new Error(`ENOENT: no such file or directory, '${skillPath}'`) as Error & { code: string };\n error.code = 'ENOENT';\n return error;\n}\n\n/**\n * Sandbox-backed skill fallback that stays inert until the session sandbox is\n * actually materialized. Skill discovery runs on latency-sensitive paths (the\n * Factory start coordinator resolves the kickoff skill before the start route\n * responds); without this guard the first project-root read would hit the lazy\n * sandbox handle and force full provisioning + repo materialization. While the\n * sandbox is unmaterialized, project skill roots simply appear empty — bundled\n * Factory skills resolve from local disk via `FactorySkillSource`. Once the\n * sandbox exists, every call delegates straight through.\n */\nclass UnmaterializedAwareSkillSource implements SkillSource {\n constructor(\n readonly fallback: SkillSource,\n readonly isMaterialized: () => boolean,\n ) {}\n\n async exists(skillPath: string): Promise<boolean> {\n return this.isMaterialized() ? this.fallback.exists(skillPath) : false;\n }\n\n async stat(skillPath: string): Promise<SkillSourceStat> {\n if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);\n return this.fallback.stat(skillPath);\n }\n\n async readFile(skillPath: string): Promise<string | Buffer> {\n if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);\n return this.fallback.readFile(skillPath);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n return this.isMaterialized() ? this.fallback.readdir(skillPath) : [];\n }\n\n realpath(skillPath: string): Promise<string> {\n if (!this.isMaterialized()) return Promise.resolve(skillPath);\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\nconst factorySkillExtension: WorkspaceSkillExtension = {\n id: 'web-factory',\n paths: [FACTORY_SKILLS_MOUNT],\n createSource: (fallback, fallbackSkillRoots) => new FactorySkillSource(fallback, fallbackSkillRoots),\n};\n\ntype DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];\n\nexport interface CreateWorkspaceFactoryOptions {\n /** Factory sandbox runtime config (template machine + workdir base). */\n sandbox?: MastraFactorySandboxConfig;\n /** GitHub integration used to resolve Factory sessions and mint repo tokens. */\n github?: GithubIntegration;\n /** Fleet the per-session sandboxes are provisioned/reattached through. */\n fleet?: SandboxFleet;\n /** Work-items storage used to resolve the session's run-binding role, so\n * review-board sessions get the reviewer PAT as `GH_TOKEN`. Optional —\n * without it every session uses the default (worker) PAT. */\n workItems?: Pick<WorkItemsStorage, 'findRunBindingBySession'>;\n /** Runtime workspace/token registrations invalidated when a session retires. */\n workspaceRegistry?: FactoryWorkspaceRegistry;\n}\n\ntype WorkspaceUnregister = () => Promise<void> | void;\n\n/** Tracks dynamic Factory workspaces by persisted session id for retirement. */\nexport class FactoryWorkspaceRegistry {\n readonly #entries = new Map<string, Map<string, WorkspaceUnregister>>();\n readonly #generations = new Map<string, number>();\n\n generation(sessionId: string): number {\n return this.#generations.get(sessionId) ?? 0;\n }\n\n async register(\n sessionId: string,\n workspaceId: string,\n generation: number,\n unregister: WorkspaceUnregister,\n ): Promise<boolean> {\n if (generation !== this.generation(sessionId)) {\n await unregister();\n return false;\n }\n const entries = this.#entries.get(sessionId) ?? new Map<string, WorkspaceUnregister>();\n entries.set(workspaceId, unregister);\n this.#entries.set(sessionId, entries);\n return true;\n }\n\n async invalidateSession(sessionId: string): Promise<void> {\n this.#generations.set(sessionId, this.generation(sessionId) + 1);\n const entries = this.#entries.get(sessionId);\n if (!entries) return;\n this.#entries.delete(sessionId);\n const results = await Promise.allSettled([...entries.values()].map(unregister => unregister()));\n const failure = results.find(result => result.status === 'rejected');\n if (failure?.status === 'rejected') throw failure.reason;\n }\n}\n\nexport function createWorkspaceFactory(options: CreateWorkspaceFactoryOptions = {}) {\n const { sandbox: sandboxConfig, github, fleet, workItems } = options;\n const workspaceRegistry = options.workspaceRegistry ?? new FactoryWorkspaceRegistry();\n const isLocalSandbox = sandboxConfig?.machine instanceof LocalSandbox;\n type GithubTokenRegistration = {\n inject: (token: string) => void;\n patKind: GithubPatKind;\n ghToken: string;\n generation: number;\n tokenReplacementPending: boolean;\n };\n type FleetSandbox = Awaited<ReturnType<SandboxFleet['ensureSandbox']>>;\n const githubTokenInjectors = new Map<string, GithubTokenRegistration>();\n const githubTokenReconciliations = new Map<string, Promise<void>>();\n // Concurrent requests for the same session (thread list + activity polling +\n // chat) must not each provision a sandbox and clone the repository. The\n // first caller materializes; followers await the same promise. Failed\n // materializations are dropped from the map so the next use retries.\n const inflightMaterializations = new Map<string, Promise<FleetSandbox>>();\n // Fully materialized sandboxes, keyed by workspace id. The lazy sandbox\n // handle delegates here once materialization completed.\n const materializedSandboxes = new Map<string, FleetSandbox>();\n // Workspace identity cache: concurrent resolutions of the same session must\n // observe the same Workspace object even when no Mastra registry is wired\n // (the registry stays the source of truth when present).\n const constructedWorkspaces = new Map<string, Workspace>();\n\n return async ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => {\n const effectiveSkillExtension = skillExtension ?? factorySkillExtension;\n const ctx = requestContext.get('controller') as AgentControllerRequestContext<MastraCodeState> | undefined;\n const session =\n ctx?.resourceId && github ? await github.sourceControlStorage.sessions.getBySessionId(ctx.resourceId) : null;\n\n if (!session) {\n if (sandboxConfig && !isLocalSandbox) {\n // Chat-only session on a remote-sandbox deploy: there is no repository\n // to materialize, and the server host must never execute commands on a\n // shared deployment. Run the session without a workspace (chat works,\n // workspace tools are simply not registered) instead of erroring on\n // every message.\n return undefined;\n }\n return getDynamicWorkspace({ requestContext, mastra, skillExtension: effectiveSkillExtension });\n }\n\n const user = getFactoryAuthUserFromContext(requestContext);\n const userId = getFactoryAuthUserId(user);\n // No identity at all is a server-side caller that forgot to seed one\n // (webhook, cron), not someone reaching for another user's session.\n if (!user?.organizationId || !userId) {\n throw new Error(`Factory session ${session.sessionId} was resolved without a caller identity`);\n }\n // Org-visible sessions open to any member of the owning organization;\n // only private sessions stay owner-only. Cross-org access never passes.\n if (user.organizationId !== session.orgId || (session.visibility === 'private' && userId !== session.userId)) {\n throw new Error(`Factory session ${session.sessionId} is not available to the current user`);\n }\n if (!sandboxConfig || !github || !fleet) {\n throw new Error('GitHub and sandbox providers are required to create a Factory session workspace');\n }\n\n const storage = github.sourceControlStorage;\n const projectRepository = await storage.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);\n // The remaining reads only depend on the repository link — issue them in\n // parallel instead of paying four sequential storage round-trips.\n const [connection, repository] = await Promise.all([\n storage.connections.get({ orgId: session.orgId, id: projectRepository.connectionId }),\n storage.repositories.get({ orgId: session.orgId, id: projectRepository.repositoryId }),\n ]);\n if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);\n const installation = await storage.installations.get({ orgId: session.orgId, id: connection.installationId });\n if (!installation) throw new Error(`GitHub installation ${connection.installationId} was not found`);\n const repoFullName = repository.slug;\n\n let workdir = isLocalSandbox\n ? fleet.computeLocalSessionWorkdir(repoFullName, session.id)\n : (session.sandboxWorkdir ?? projectRepository.sandboxWorkdir);\n // The system prompt derives its working directory from `state.projectPath`\n // and falls back to the server's own process.cwd() when unset — which\n // points the agent at the host checkout (and lets it run `git checkout`\n // there instead of in its session workdir). Pin it to the session workdir.\n // During createSession this seeds the session's initial state (the\n // workspace resolves before the session is built); on later requests it\n // self-heals live state.\n let stateSeeded = false;\n if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {\n await ctx.setState({ projectPath: workdir, projectName: repoFullName });\n stateSeeded = true;\n }\n const binding: SandboxBindingStore = {\n // Read through to the session row so teardown after a fresh provision\n // sees the just-persisted id instead of a stale snapshot.\n get sandboxId() {\n return session.sandboxId;\n },\n checkpointName: checkpointNameForSession(session.id),\n // Boot-only fallback: a brand-new session (no session checkpoint yet)\n // seeds from the repo's warm base checkpoint when one is available and\n // still matches the current setup command. Snapshots keep writing to\n // the session checkpoint, so the shared base image is never mutated.\n ...(!session.materializedAt && projectRepository.baseCheckpoint && !baseCheckpointIsStale(projectRepository)\n ? { seedCheckpointName: projectRepository.baseCheckpoint.name }\n : {}),\n setSandboxId: async id => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: id, sandboxWorkdir: workdir });\n session.sandboxId = id;\n session.sandboxWorkdir = workdir;\n },\n clear: async () => {\n await storage.sessions.setSandbox({ id: session.id, sandboxId: null, sandboxWorkdir: workdir });\n session.sandboxId = null;\n },\n };\n\n const extensionId = effectiveSkillExtension ? `-${effectiveSkillExtension.id}` : '';\n const workspaceId = `${WORKSPACE_ID_PREFIX}-${projectRepository.id}-${session.id}${extensionId}`;\n const workspaceGeneration = workspaceRegistry.generation(session.sessionId);\n const configDir = sandboxConfig.workdir ?? DEFAULT_CONFIG_DIR;\n\n const getRepositoryToken = async (): Promise<string> => {\n const access = await github.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n return token;\n };\n const resolveGithubPatKind = async (fallback: GithubPatKind): Promise<GithubPatKind> => {\n if (!workItems) return 'default';\n try {\n const address = getFactorySessionAddress(requestContext);\n const runBinding = address ? await workItems.findRunBindingBySession(address) : null;\n return runBinding?.role === 'review' && runBinding.status === 'active' && runBinding.orgId === session.orgId\n ? 'reviewer'\n : 'default';\n } catch {\n // Preserve the installed role when binding storage is temporarily unavailable.\n return fallback;\n }\n };\n const registerGithubTokenContext = (registered: GithubTokenRegistration): void => {\n const generation = registered.generation;\n registerGithubTokenInjector(requestContext, token => {\n if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) {\n throw new Error('GitHub token refresh no longer matches the active Factory workspace role.');\n }\n registered.inject(token);\n });\n registerGithubPatKind(requestContext, registered.patKind);\n };\n const reconcileGithubToken = async (): Promise<void> => {\n const previous = githubTokenReconciliations.get(workspaceId) ?? Promise.resolve();\n const reconciliation = previous\n .catch(() => {})\n .then(async () => {\n const registered = githubTokenInjectors.get(workspaceId);\n if (!registered) return;\n\n const previousPatKind = registered.patKind;\n const patKind = await resolveGithubPatKind(previousPatKind);\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (patKind !== previousPatKind) {\n registered.patKind = patKind;\n registered.generation += 1;\n }\n if (patKind === 'reviewer') registered.tokenReplacementPending = false;\n if (previousPatKind === 'reviewer' && patKind === 'default') {\n // Invalidate reviewer refresh contexts before replacement I/O so\n // they cannot restore reviewer credentials after a failed downgrade.\n registered.tokenReplacementPending = true;\n }\n\n let token = await getGithubPat(() => github.integrationStorage, session.orgId, patKind);\n if (!token && registered.tokenReplacementPending) token = await getRepositoryToken();\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (token && token !== registered.ghToken) {\n try {\n registered.inject(token);\n } catch (error) {\n if (registered.tokenReplacementPending) throw error;\n // Same-role rotations and reviewer upgrades remain best-effort.\n }\n }\n if (token && token === registered.ghToken) registered.tokenReplacementPending = false;\n registerGithubTokenContext(registered);\n });\n githubTokenReconciliations.set(workspaceId, reconciliation);\n try {\n await reconciliation;\n } finally {\n if (githubTokenReconciliations.get(workspaceId) === reconciliation) {\n githubTokenReconciliations.delete(workspaceId);\n }\n }\n };\n const reconcileRegisteredWorkspace = async (workspace: Workspace): Promise<Workspace> => {\n const registered = githubTokenInjectors.get(workspaceId);\n try {\n await reconcileGithubToken();\n } catch (error) {\n if (registered?.tokenReplacementPending && githubTokenInjectors.get(workspaceId) === registered) {\n // The role generation already invalidated reviewer refresh contexts.\n // Keep the pending registration so failed eviction cannot make a\n // still-live reviewer workspace look safe on the next reuse.\n let evicted = false;\n try {\n evicted = (await mastra?.removeWorkspace?.(workspaceId)) === true;\n } catch {\n // Preserve the credential-replacement error and retry on the next reuse.\n }\n try {\n await workspace.destroy();\n evicted = true;\n } catch {\n // The pending registration keeps the workspace quarantined if cleanup also fails.\n }\n if (evicted && githubTokenInjectors.get(workspaceId) === registered) {\n githubTokenInjectors.delete(workspaceId);\n materializedSandboxes.delete(workspaceId);\n constructedWorkspaces.delete(workspaceId);\n }\n }\n throw error;\n }\n if (registered && githubTokenInjectors.get(workspaceId) !== registered) {\n throw new Error('Factory workspace GitHub credential registration is no longer active.');\n }\n return workspace;\n };\n\n let existing: Workspace | undefined;\n try {\n existing = mastra?.getWorkspaceById(workspaceId) as Workspace | undefined;\n } catch {\n // Not registered yet.\n existing = undefined;\n }\n existing ??= constructedWorkspaces.get(workspaceId);\n if (existing) {\n existing.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);\n // A materialization kicked off by another caller may still be running.\n // Deliberately do NOT wait for it: a metadata-only resolution (thread\n // list, messages, activity) must not block on the clone/setup that lazy\n // materialization exists to avoid. Token reconciliation below no-ops\n // until the leader registers the injector, and the next reuse after\n // materialization completes reconciles against the live sandbox.\n return reconcileRegisteredWorkspace(existing);\n }\n\n const retiredError = () =>\n new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);\n const materializeSandbox = async (): Promise<FleetSandbox> => {\n // A session already retired by the time a held lazy handle re-enters\n // materialization must not provision anything — bail before touching\n // the pool or the fleet budget.\n if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {\n throw retiredError();\n }\n // A terminal work item or a deleted session may have returned a\n // still-warm VM — with this repository already cloned — to the reuse\n // pool. Adopt it before provisioning a fresh sandbox. Pooled VMs carry\n // no credentials (tokens are injected per command, and the workdir is\n // scrubbed on release and again below), so any user's session for this\n // repository can claim one.\n let claimedPooledSandbox = false;\n if (!isLocalSandbox && !session.sandboxId) {\n const pooled = await storage.sandboxPool.claim({\n projectRepositoryId: session.projectRepositoryId,\n });\n if (pooled) {\n await storage.sessions.setSandbox({\n id: session.id,\n sandboxId: pooled.sandboxId,\n sandboxWorkdir: pooled.sandboxWorkdir,\n });\n session.sandboxId = pooled.sandboxId;\n session.sandboxWorkdir = pooled.sandboxWorkdir;\n workdir = pooled.sandboxWorkdir;\n claimedPooledSandbox = true;\n }\n }\n\n const token = await getRepositoryToken();\n\n // The `gh` CLI needs a PAT when the org configured one (installation\n // tokens 403 on integration-restricted endpoints); git clone/checkout\n // below keep using the minted installation token. Review-board sessions\n // (run-binding role `review`) authenticate `gh` as the reviewer account\n // when a reviewer token is configured; everything else — including\n // sessions with no resolvable run binding — uses the worker token.\n const patKind = await resolveGithubPatKind('default');\n const ghCliToken = (await getGithubPat(() => github.integrationStorage, session.orgId, patKind)) ?? token;\n\n const ensureSandbox = () =>\n fleet.ensureSandbox(binding, { GH_TOKEN: ghCliToken }, undefined, {\n ...(isLocalSandbox ? { workingDirectory: workdir } : {}),\n actingUserId: userId,\n });\n const runMaterialize = (target: Awaited<ReturnType<typeof ensureSandbox>>, skipPull: boolean) =>\n materializeRepo({\n row: { id: session.id, sandboxWorkdir: workdir, materializedAt: session.materializedAt },\n repoInfo: { repoFullName: repoFullName, defaultBranch: repository.defaultBranch },\n sandbox: target,\n token,\n storage: storage.sessions,\n // A checkpoint-seeded checkout is already at (or minutes behind) the\n // default branch HEAD — skip the redundant network pull so the first\n // agent turn isn't stalled behind it.\n skipPullOnExistingCheckout: skipPull,\n });\n const isGitMissing = (error: unknown) => error instanceof MaterializeError && error.code === 'git-missing';\n\n let sandbox = await ensureSandbox();\n // A claimed VM still has the previous session's branch checked out —\n // reset it to the default branch before materialize/checkout. When the\n // pooled VM was already reaped, `ensureSandbox` provisioned fresh and\n // the recycle is a no-op (no checkout on disk yet).\n if (claimedPooledSandbox) await recycleClaimedWorkdir(sandbox, workdir, repository.defaultBranch);\n // A never-materialized session whose workdir already holds this repo's\n // checkout was seeded from the warm base checkpoint — the setup command\n // already ran during the base build, so skip it below.\n const seededFromBaseCheckpoint =\n !!binding.seedCheckpointName &&\n !session.materializedAt &&\n !claimedPooledSandbox &&\n (await hasExistingCheckout(sandbox, workdir, repoFullName));\n try {\n await runMaterialize(sandbox, seededFromBaseCheckpoint);\n } catch (error) {\n if (!isGitMissing(error)) throw error;\n // A sandbox without git was booted from a bare base image (e.g. the\n // platform proxy falls back to a clean Debian base when its template\n // build fails). That VM can never materialize a repo, and its id is\n // already persisted on the binding — tear it down so re-opens stop\n // reattaching to the poisoned sandbox, then retry once on a fresh VM.\n await fleet.teardownSandbox(binding, sandbox);\n sandbox = await ensureSandbox();\n try {\n // The retry runs on a freshly provisioned VM with no checkout, so\n // the skip flag is moot — pass false to take the normal clone path.\n await runMaterialize(sandbox, false);\n } catch (retryError) {\n // Still bare — the provider's template is persistently broken.\n // Clear the binding so a later manual retry provisions fresh.\n if (isGitMissing(retryError)) await fleet.teardownSandbox(binding, sandbox);\n throw retryError;\n }\n }\n await checkoutSessionBranch(sandbox, workdir, {\n branch: session.branch,\n baseBranch: session.baseBranch || projectRepository.branch || repository.defaultBranch,\n token,\n repoFullName: repoFullName,\n });\n // A checkpoint-seeded checkout already ran the setup command during the\n // base build, so re-running it here is pure latency.\n if (projectRepository.setupCommand && !seededFromBaseCheckpoint) {\n try {\n await runWorktreeSetup(sandbox, workdir, projectRepository.setupCommand);\n } catch (setupError) {\n if (projectRepository.teardownCommand) {\n try {\n await runWorktreeTeardown(sandbox, workdir, projectRepository.teardownCommand, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (teardownError) {\n console.warn('[Mastra Factory] Worktree teardown after setup failure failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: teardownError instanceof Error ? teardownError.message.slice(-2000) : String(teardownError),\n });\n }\n }\n throw setupError;\n }\n }\n\n const tokenRegistration: GithubTokenRegistration = {\n inject: freshToken => {\n if (!sandbox.setEnvironmentVariable) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n sandbox.setEnvironmentVariable('GH_TOKEN', freshToken);\n tokenRegistration.ghToken = freshToken;\n },\n patKind,\n ghToken: ghCliToken,\n generation: 0,\n tokenReplacementPending: false,\n };\n // The session can be retired while this deferred phase is in flight.\n // Registration happened back at construction time (the workspace exists\n // before it materializes), so the retirement callback has already torn\n // the workspace down — tear the just-built sandbox back down (freeing\n // its binding and, on remote providers, its fleet budget slot) and\n // surface the retirement to the caller instead of handing back a\n // sandbox belonging to a dead session.\n if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {\n try {\n await fleet.teardownSandbox(binding, sandbox);\n } catch (teardownError) {\n console.warn('[Mastra Factory] Sandbox teardown after mid-materialization retirement failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n error: teardownError instanceof Error ? teardownError.message.slice(-2000) : String(teardownError),\n });\n }\n throw retiredError();\n }\n githubTokenInjectors.set(workspaceId, tokenRegistration);\n registerGithubTokenContext(tokenRegistration);\n return sandbox;\n };\n\n // Memoized deferred phase. The first caller (a background warm-up at\n // session start, or the first FS/sandbox operation) materializes; followers\n // await the same in-flight promise. Failures are dropped from the map so\n // the next use retries instead of caching a broken sandbox.\n const ensureMaterialized = async (): Promise<FleetSandbox> => {\n const ready = materializedSandboxes.get(workspaceId);\n if (ready) return ready;\n let inflight = inflightMaterializations.get(workspaceId);\n if (!inflight) {\n inflight = materializeSandbox();\n inflightMaterializations.set(workspaceId, inflight);\n inflight.then(\n sb => {\n materializedSandboxes.set(workspaceId, sb);\n // Project skill roots (.claude/skills etc.) were reported empty by\n // the unmaterialized-source guard during discovery; rescan now that\n // the checkout exists so repo-local skills become visible without\n // waiting for the maybeRefresh cooldown. Fire-and-forget.\n void workspace.skills?.refresh().catch(() => {});\n },\n () => {},\n );\n }\n try {\n return await inflight;\n } finally {\n if (inflightMaterializations.get(workspaceId) === inflight) {\n inflightMaterializations.delete(workspaceId);\n }\n }\n };\n\n // Lazy sandbox handle: resolution returns immediately and the sandbox\n // work (provision/boot-from-checkpoint + materialize + checkout + setup)\n // runs on first use. Metadata-only resolutions (thread-list polling)\n // never touch it.\n const lazySandbox = {\n get id() {\n return materializedSandboxes.get(workspaceId)?.id ?? workspaceId;\n },\n name: 'Factory Lazy Sandbox',\n get provider() {\n return fleet.provider;\n },\n get status() {\n return materializedSandboxes.has(workspaceId) ? 'ready' : 'pending';\n },\n get supportsCheckpoints() {\n return materializedSandboxes.get(workspaceId)?.supportsCheckpoints ?? false;\n },\n getInstructions() {\n // Prefer the live sandbox's instructions once materialized; before\n // that, forward the configured template machine's instructions so\n // tool descriptions are accurate without forcing materialization.\n return materializedSandboxes.get(workspaceId)?.getInstructions?.() ?? fleet.getInstructions();\n },\n clone(): never {\n throw new Error('The Factory session sandbox cannot be cloned from a lazy handle.');\n },\n async start() {\n // Intentionally a no-op. `Workspace.init()` calls `sandbox.start()`\n // during session creation, and sessions are get-or-created by\n // metadata-only GET routes (/threads, /messages). Materializing here\n // would provision a sandbox for every read-only poll. The sandbox\n // materializes on first real use (executeCommand/getInfo) instead.\n },\n async getInfo() {\n const sandbox = await ensureMaterialized();\n return sandbox.getInfo();\n },\n async executeCommand(command: string, args?: string[], options?: Record<string, unknown>) {\n const sandbox = await ensureMaterialized();\n try {\n return await sandbox.executeCommand(command, args, options);\n } catch (error) {\n if (!isDeadSandboxError(error) && !(isLocalSandbox && isMissingWorkdirError(error, workdir))) throw error;\n // The sandbox died mid-session (idle GC, provider destroy, broken\n // transport, or a retired local checkout removed from under us).\n // Drop the dead handle and re-run the materialization\n // pipeline — fleet's ensureSandbox walks the revival ladder\n // (reattach → checkpoint-seeded provision → fresh clone) — then\n // retry the command once. Concurrent failures coalesce onto the\n // same revival through `inflightMaterializations`.\n if (materializedSandboxes.get(workspaceId) === sandbox) {\n materializedSandboxes.delete(workspaceId);\n }\n const revived = await ensureMaterialized();\n return revived.executeCommand(command, args, options);\n }\n },\n setEnvironmentVariable(name: string, value: string) {\n const sandbox = materializedSandboxes.get(workspaceId);\n if (!sandbox?.setEnvironmentVariable) {\n throw new Error('The Factory session sandbox is not materialized yet.');\n }\n sandbox.setEnvironmentVariable(name, value);\n },\n async snapshot() {\n // Nothing to checkpoint before the sandbox exists.\n await materializedSandboxes.get(workspaceId)?.snapshot?.();\n },\n async stop() {\n await materializedSandboxes.get(workspaceId)?.stop?.();\n },\n };\n\n const filesystem = new SandboxFilesystem({\n id: `sandbox-fs:${workspaceId}:${workdir}`,\n sandbox: lazySandbox,\n workdir,\n });\n const projectSkillPaths = [path.join(configDir, 'skills'), '.claude/skills', '.agents/skills'];\n const guardedSkillFallback = new UnmaterializedAwareSkillSource(filesystem, () =>\n materializedSandboxes.has(workspaceId),\n );\n const skillPaths = [...(effectiveSkillExtension?.paths ?? []), ...projectSkillPaths];\n const workspace = new Workspace({\n id: workspaceId,\n name: 'Mastra Code Factory Session Workspace',\n filesystem,\n sandbox: lazySandbox as unknown as ConstructorParameters<typeof Workspace>[0]['sandbox'],\n tools: MASTRACODE_WORKSPACE_TOOLS,\n skills: skillPaths,\n // Project skill roots live in the sandbox checkout; guard them so skill\n // discovery before materialization (e.g. kickoff skill resolution in the\n // start coordinator) never forces sandbox provisioning.\n skillSource:\n effectiveSkillExtension?.createSource(guardedSkillFallback, projectSkillPaths) ?? guardedSkillFallback,\n });\n // Register with the Mastra instance so sync HTTP handlers that resolve\n // the workspace via `mastra.getWorkspaceById(id)` (file tree, permissions\n // probe, MCP/tool routes) find it instead of throwing\n // `MASTRA_GET_WORKSPACE_BY_ID_NOT_FOUND`. `addWorkspace` is idempotent on\n // key collision, so concurrent first resolutions stay race-safe (the\n // deferred phase is deduped separately through `inflightMaterializations`).\n mastra?.addWorkspace(workspace, workspaceId, { source: 'mastra' });\n // Cache synchronously with construction: the `await` below is a suspension\n // point, and a concurrent resolution for the same session must observe this\n // workspace rather than build a second one.\n constructedWorkspaces.set(workspaceId, workspace);\n // Retirement is registered against the workspace itself rather than the\n // sandbox: construction is now eager while materialization is deferred, so\n // a session retired before its first tool call still has a workspace (and a\n // token injector) that must be torn down.\n const registered = await workspaceRegistry.register(\n session.sessionId,\n workspaceId,\n workspaceGeneration,\n async () => {\n githubTokenInjectors.delete(workspaceId);\n materializedSandboxes.delete(workspaceId);\n constructedWorkspaces.delete(workspaceId);\n await mastra?.removeWorkspace?.(workspaceId);\n },\n );\n if (!registered) {\n throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);\n }\n\n // Session start (the resolution that seeds the session's initial state)\n // warms the sandbox in the background so it materializes in parallel with\n // the model's first turn instead of on the first tool call.\n if (stateSeeded) {\n void ensureMaterialized().catch(error => {\n console.error(`[factory:workspace] background materialization for ${workspaceId} failed`, error);\n });\n }\n return workspace;\n };\n}\n\nexport const getFactoryWorkspace = createWorkspaceFactory();\n"],"mappings":";;;;;;;;;;;;;;;AAiCA,MAAM,sBAAsB;AAC5B,MAAM,4BAA4B;AAElC,SAAgB,yBAAyB,WAA2B;CAClE,OAAO,GAAG,0BAA0B,GAAG;AACzC;;;;;;;;;;;;;;;;AAiBA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,IAAI,MAAM,SAAS,yBAAyB,OAAO;CACnD,IAAI,MAAM,SAAS,6BACjB,OAAQ,MAAuC,WAAW;CAE5D,OAAO,oDAAoD,KAAK,MAAM,OAAO;AAC/E;;;;;;;;;;;;AAaA,SAAgB,sBAAsB,OAAgB,SAAsC;CAC1F,IAAI,CAAC,SAAS,OAAO;CACrB,IAAK,OAAwC,SAAS,UAAU,OAAO;CACvE,OAAO,CAAC,WAAW,OAAO;AAC5B;AAEA,MAAM,kBAAkB,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC;AAC9D,MAAM,2BAA2B,KAAK,iBAAiB,gBAAgB;AACvE,MAAa,6BACX;CAGE;CAGA,KAAK,iBAAiB,MAAM,gBAAgB;CAE5C,KAAK,QAAQ,IAAI,GAAG,OAAO,UAAU,UAAU,gBAAgB;AACjE,CAAC,CAAC,KAAK,UAAU,KAAK;AACxB,MAAM,uBAAuB,KAAK,QAAQ,KAAK,MAAM,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,+BAA+B;AACzG,MAAa,sCAAsB,IAAI,IAAI;CACzC;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,IAAM,qBAAN,MAAgD;CAKnC;CAJX,iBAA0B,IAAI,iBAAiB,EAAE,UAAU,2BAA2B,CAAC;CACvF;CAEA,YACE,UACA,oBACA;EAFS,KAAA,WAAA;EAGT,KAAKC,sBAAsB,IAAI,IAAI,mBAAmB,KAAI,cAAa,KAAK,UAAU,SAAS,CAAC,CAAC;CACnG;CAEA,eAAe,WAA4B;EACzC,MAAM,aAAa,KAAK,UAAU,SAAS;EAC3C,OAAO,eAAe,wBAAwB,WAAW,WAAW,GAAG,uBAAuB,KAAK,KAAK;CAC1G;CAEA,aAAa,WAA2B;EACtC,OAAO,KAAK,SAAS,sBAAsB,KAAK,UAAU,SAAS,CAAC;CACtE;CAEA,OAAO,WAAqC;EAC1C,OAAO,KAAKC,eAAe,SAAS,IAChC,KAAKF,eAAe,OAAO,KAAKG,aAAa,SAAS,CAAC,IACvD,KAAK,SAAS,OAAO,SAAS;CACpC;CAEA,KAAK,WAA6C;EAChD,OAAO,KAAKD,eAAe,SAAS,IAChC,KAAKF,eAAe,KAAK,KAAKG,aAAa,SAAS,CAAC,IACrD,KAAK,SAAS,KAAK,SAAS;CAClC;CAEA,SAAS,WAA6C;EACpD,OAAO,KAAKD,eAAe,SAAS,IAChC,KAAKF,eAAe,SAAS,KAAKG,aAAa,SAAS,CAAC,IACzD,KAAK,SAAS,SAAS,SAAS;CACtC;CAEA,MAAM,QAAQ,WAAgD;EAC5D,IAAI,KAAKD,eAAe,SAAS,GAC/B,OAAO,KAAKF,eAAe,QAAQ,KAAKG,aAAa,SAAS,CAAC;EAEjE,MAAM,UAAU,MAAM,KAAK,SAAS,QAAQ,SAAS;EACrD,IAAI,KAAKF,oBAAoB,IAAI,KAAK,UAAU,SAAS,CAAC,GACxD,OAAO,QAAQ,QAAO,UAAS,CAAC,oBAAoB,IAAI,MAAM,IAAI,CAAC;EAErE,OAAO;CACT;CAEA,SAAS,WAAoC;EAC3C,IAAI,KAAKC,eAAe,SAAS,GAAG,OAAO,QAAQ,QAAQ,KAAK,UAAU,SAAS,CAAC;EACpF,OAAO,KAAK,SAAS,WAAW,KAAK,SAAS,SAAS,SAAS,IAAI,QAAQ,QAAQ,SAAS;CAC/F;AACF;AAEA,SAAS,kBAAkB,WAA0B;CACnD,MAAM,wBAAQ,IAAI,MAAM,uCAAuC,UAAU,EAAE;CAC3E,MAAM,OAAO;CACb,OAAO;AACT;;;;;;;;;;;AAYA,IAAM,iCAAN,MAA4D;CAE/C;CACA;CAFX,YACE,UACA,gBACA;EAFS,KAAA,WAAA;EACA,KAAA,iBAAA;CACR;CAEH,MAAM,OAAO,WAAqC;EAChD,OAAO,KAAK,eAAe,IAAI,KAAK,SAAS,OAAO,SAAS,IAAI;CACnE;CAEA,MAAM,KAAK,WAA6C;EACtD,IAAI,CAAC,KAAK,eAAe,GAAG,MAAM,kBAAkB,SAAS;EAC7D,OAAO,KAAK,SAAS,KAAK,SAAS;CACrC;CAEA,MAAM,SAAS,WAA6C;EAC1D,IAAI,CAAC,KAAK,eAAe,GAAG,MAAM,kBAAkB,SAAS;EAC7D,OAAO,KAAK,SAAS,SAAS,SAAS;CACzC;CAEA,MAAM,QAAQ,WAAgD;EAC5D,OAAO,KAAK,eAAe,IAAI,KAAK,SAAS,QAAQ,SAAS,IAAI,CAAC;CACrE;CAEA,SAAS,WAAoC;EAC3C,IAAI,CAAC,KAAK,eAAe,GAAG,OAAO,QAAQ,QAAQ,SAAS;EAC5D,OAAO,KAAK,SAAS,WAAW,KAAK,SAAS,SAAS,SAAS,IAAI,QAAQ,QAAQ,SAAS;CAC/F;AACF;AAEA,MAAM,wBAAiD;CACrD,IAAI;CACJ,OAAO,CAAC,oBAAoB;CAC5B,eAAe,UAAU,uBAAuB,IAAI,mBAAmB,UAAU,kBAAkB;AACrG;;AAsBA,IAAa,2BAAb,MAAsC;CACpC,2BAAoB,IAAI,IAA8C;CACtE,+BAAwB,IAAI,IAAoB;CAEhD,WAAW,WAA2B;EACpC,OAAO,KAAKG,aAAa,IAAI,SAAS,KAAK;CAC7C;CAEA,MAAM,SACJ,WACA,aACA,YACA,YACkB;EAClB,IAAI,eAAe,KAAK,WAAW,SAAS,GAAG;GAC7C,MAAM,WAAW;GACjB,OAAO;EACT;EACA,MAAM,UAAU,KAAKD,SAAS,IAAI,SAAS,qBAAK,IAAI,IAAiC;EACrF,QAAQ,IAAI,aAAa,UAAU;EACnC,KAAKA,SAAS,IAAI,WAAW,OAAO;EACpC,OAAO;CACT;CAEA,MAAM,kBAAkB,WAAkC;EACxD,KAAKC,aAAa,IAAI,WAAW,KAAK,WAAW,SAAS,IAAI,CAAC;EAC/D,MAAM,UAAU,KAAKD,SAAS,IAAI,SAAS;EAC3C,IAAI,CAAC,SAAS;EACd,KAAKA,SAAS,OAAO,SAAS;EAE9B,MAAM,WAAU,MADM,QAAQ,WAAW,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,KAAI,eAAc,WAAW,CAAC,CAAC,EAAA,CACtE,MAAK,WAAU,OAAO,WAAW,UAAU;EACnE,IAAI,SAAS,WAAW,YAAY,MAAM,QAAQ;CACpD;AACF;AAEA,SAAgB,uBAAuB,UAAyC,CAAC,GAAG;CAClF,MAAM,EAAE,SAAS,eAAe,QAAQ,OAAO,cAAc;CAC7D,MAAM,oBAAoB,QAAQ,qBAAqB,IAAI,yBAAyB;CACpF,MAAM,iBAAiB,eAAe,mBAAmB;CASzD,MAAM,uCAAuB,IAAI,IAAqC;CACtE,MAAM,6CAA6B,IAAI,IAA2B;CAKlE,MAAM,2CAA2B,IAAI,IAAmC;CAGxE,MAAM,wCAAwB,IAAI,IAA0B;CAI5D,MAAM,wCAAwB,IAAI,IAAuB;CAEzD,OAAO,OAAO,EAAE,gBAAgB,QAAQ,qBAA8C;EACpF,MAAM,0BAA0B,kBAAkB;EAClD,MAAM,MAAM,eAAe,IAAI,YAAY;EAC3C,MAAM,UACJ,KAAK,cAAc,SAAS,MAAM,OAAO,qBAAqB,SAAS,eAAe,IAAI,UAAU,IAAI;EAE1G,IAAI,CAAC,SAAS;GACZ,IAAI,iBAAiB,CAAC,gBAMpB;GAEF,OAAO,oBAAoB;IAAE;IAAgB;IAAQ,gBAAgB;GAAwB,CAAC;EAChG;EAEA,MAAM,OAAO,8BAA8B,cAAc;EACzD,MAAM,SAAS,qBAAqB,IAAI;EAGxC,IAAI,CAAC,MAAM,kBAAkB,CAAC,QAC5B,MAAM,IAAI,MAAM,mBAAmB,QAAQ,UAAU,wCAAwC;EAI/F,IAAI,KAAK,mBAAmB,QAAQ,SAAU,QAAQ,eAAe,aAAa,WAAW,QAAQ,QACnG,MAAM,IAAI,MAAM,mBAAmB,QAAQ,UAAU,sCAAsC;EAE7F,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,OAChC,MAAM,IAAI,MAAM,iFAAiF;EAGnG,MAAM,UAAU,OAAO;EACvB,MAAM,oBAAoB,MAAM,QAAQ,oBAAoB,IAAI;GAC9D,OAAO,QAAQ;GACf,IAAI,QAAQ;EACd,CAAC;EACD,IAAI,CAAC,mBAAmB,MAAM,IAAI,MAAM,mBAAmB,QAAQ,oBAAoB,eAAe;EAGtG,MAAM,CAAC,YAAY,cAAc,MAAM,QAAQ,IAAI,CACjD,QAAQ,YAAY,IAAI;GAAE,OAAO,QAAQ;GAAO,IAAI,kBAAkB;EAAa,CAAC,GACpF,QAAQ,aAAa,IAAI;GAAE,OAAO,QAAQ;GAAO,IAAI,kBAAkB;EAAa,CAAC,CACvF,CAAC;EACD,IAAI,CAAC,cAAc,CAAC,YAAY,MAAM,IAAI,MAAM,mBAAmB,QAAQ,oBAAoB,eAAe;EAE9G,IAAI,CAAC,MADsB,QAAQ,cAAc,IAAI;GAAE,OAAO,QAAQ;GAAO,IAAI,WAAW;EAAe,CAAC,GACzF,MAAM,IAAI,MAAM,uBAAuB,WAAW,eAAe,eAAe;EACnG,MAAM,eAAe,WAAW;EAEhC,IAAI,UAAU,iBACV,MAAM,2BAA2B,cAAc,QAAQ,EAAE,IACxD,QAAQ,kBAAkB,kBAAkB;EAQjD,IAAI,cAAc;EAClB,IAAI,OAAO,WAAW,IAAI,SAAS,CAAC,EAAE,gBAAgB,SAAS;GAC7D,MAAM,IAAI,SAAS;IAAE,aAAa;IAAS,aAAa;GAAa,CAAC;GACtE,cAAc;EAChB;EACA,MAAM,UAA+B;GAGnC,IAAI,YAAY;IACd,OAAO,QAAQ;GACjB;GACA,gBAAgB,yBAAyB,QAAQ,EAAE;GAKnD,GAAI,CAAC,QAAQ,kBAAkB,kBAAkB,kBAAkB,CAAC,sBAAsB,iBAAiB,IACvG,EAAE,oBAAoB,kBAAkB,eAAe,KAAK,IAC5D,CAAC;GACL,cAAc,OAAM,OAAM;IACxB,MAAM,QAAQ,SAAS,WAAW;KAAE,IAAI,QAAQ;KAAI,WAAW;KAAI,gBAAgB;IAAQ,CAAC;IAC5F,QAAQ,YAAY;IACpB,QAAQ,iBAAiB;GAC3B;GACA,OAAO,YAAY;IACjB,MAAM,QAAQ,SAAS,WAAW;KAAE,IAAI,QAAQ;KAAI,WAAW;KAAM,gBAAgB;IAAQ,CAAC;IAC9F,QAAQ,YAAY;GACtB;EACF;EAEA,MAAM,cAAc,0BAA0B,IAAI,wBAAwB,OAAO;EACjF,MAAM,cAAc,GAAG,oBAAoB,GAAG,kBAAkB,GAAG,GAAG,QAAQ,KAAK;EACnF,MAAM,sBAAsB,kBAAkB,WAAW,QAAQ,SAAS;EAC1E,MAAM,YAAY,cAAc,WAAW;EAE3C,MAAM,qBAAqB,YAA6B;GAKtD,MAAM,SAAQ,MAJO,OAAO,eAAe,oBAAoB;IAC7D,OAAO,QAAQ;IACf,cAAc,WAAW;GAC3B,CAAC,EAAA,CACoB,eAAe;GACpC,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,0EAA0E;GACtG,OAAO;EACT;EACA,MAAM,uBAAuB,OAAO,aAAoD;GACtF,IAAI,CAAC,WAAW,OAAO;GACvB,IAAI;IACF,MAAM,UAAU,yBAAyB,cAAc;IACvD,MAAM,aAAa,UAAU,MAAM,UAAU,wBAAwB,OAAO,IAAI;IAChF,OAAO,YAAY,SAAS,YAAY,WAAW,WAAW,YAAY,WAAW,UAAU,QAAQ,QACnG,aACA;GACN,QAAQ;IAEN,OAAO;GACT;EACF;EACA,MAAM,8BAA8B,eAA8C;GAChF,MAAM,aAAa,WAAW;GAC9B,4BAA4B,iBAAgB,UAAS;IACnD,IAAI,qBAAqB,IAAI,WAAW,MAAM,cAAc,WAAW,eAAe,YACpF,MAAM,IAAI,MAAM,2EAA2E;IAE7F,WAAW,OAAO,KAAK;GACzB,CAAC;GACD,sBAAsB,gBAAgB,WAAW,OAAO;EAC1D;EACA,MAAM,uBAAuB,YAA2B;GAEtD,MAAM,kBADW,2BAA2B,IAAI,WAAW,KAAK,QAAQ,QAAQ,EAAA,CAE7E,YAAY,CAAC,CAAC,CAAC,CACf,KAAK,YAAY;IAChB,MAAM,aAAa,qBAAqB,IAAI,WAAW;IACvD,IAAI,CAAC,YAAY;IAEjB,MAAM,kBAAkB,WAAW;IACnC,MAAM,UAAU,MAAM,qBAAqB,eAAe;IAC1D,IAAI,qBAAqB,IAAI,WAAW,MAAM,YAAY;IAE1D,IAAI,YAAY,iBAAiB;KAC/B,WAAW,UAAU;KACrB,WAAW,cAAc;IAC3B;IACA,IAAI,YAAY,YAAY,WAAW,0BAA0B;IACjE,IAAI,oBAAoB,cAAc,YAAY,WAGhD,WAAW,0BAA0B;IAGvC,IAAI,QAAQ,MAAM,mBAAmB,OAAO,oBAAoB,QAAQ,OAAO,OAAO;IACtF,IAAI,CAAC,SAAS,WAAW,yBAAyB,QAAQ,MAAM,mBAAmB;IACnF,IAAI,qBAAqB,IAAI,WAAW,MAAM,YAAY;IAE1D,IAAI,SAAS,UAAU,WAAW,SAChC,IAAI;KACF,WAAW,OAAO,KAAK;IACzB,SAAS,OAAO;KACd,IAAI,WAAW,yBAAyB,MAAM;IAEhD;IAEF,IAAI,SAAS,UAAU,WAAW,SAAS,WAAW,0BAA0B;IAChF,2BAA2B,UAAU;GACvC,CAAC;GACH,2BAA2B,IAAI,aAAa,cAAc;GAC1D,IAAI;IACF,MAAM;GACR,UAAU;IACR,IAAI,2BAA2B,IAAI,WAAW,MAAM,gBAClD,2BAA2B,OAAO,WAAW;GAEjD;EACF;EACA,MAAM,+BAA+B,OAAO,cAA6C;GACvF,MAAM,aAAa,qBAAqB,IAAI,WAAW;GACvD,IAAI;IACF,MAAM,qBAAqB;GAC7B,SAAS,OAAO;IACd,IAAI,YAAY,2BAA2B,qBAAqB,IAAI,WAAW,MAAM,YAAY;KAI/F,IAAI,UAAU;KACd,IAAI;MACF,UAAW,MAAM,QAAQ,kBAAkB,WAAW,MAAO;KAC/D,QAAQ,CAER;KACA,IAAI;MACF,MAAM,UAAU,QAAQ;MACxB,UAAU;KACZ,QAAQ,CAER;KACA,IAAI,WAAW,qBAAqB,IAAI,WAAW,MAAM,YAAY;MACnE,qBAAqB,OAAO,WAAW;MACvC,sBAAsB,OAAO,WAAW;MACxC,sBAAsB,OAAO,WAAW;KAC1C;IACF;IACA,MAAM;GACR;GACA,IAAI,cAAc,qBAAqB,IAAI,WAAW,MAAM,YAC1D,MAAM,IAAI,MAAM,uEAAuE;GAEzF,OAAO;EACT;EAEA,IAAI;EACJ,IAAI;GACF,WAAW,QAAQ,iBAAiB,WAAW;EACjD,QAAQ;GAEN,WAAW,KAAA;EACb;EACA,aAAa,sBAAsB,IAAI,WAAW;EAClD,IAAI,UAAU;GACZ,SAAS,eAAe,0BAA0B;GAOlD,OAAO,6BAA6B,QAAQ;EAC9C;EAEA,MAAM,qCACJ,IAAI,MAAM,mBAAmB,QAAQ,UAAU,8CAA8C;EAC/F,MAAM,qBAAqB,YAAmC;GAI5D,IAAI,kBAAkB,WAAW,QAAQ,SAAS,MAAM,qBACtD,MAAM,aAAa;GAQrB,IAAI,uBAAuB;GAC3B,IAAI,CAAC,kBAAkB,CAAC,QAAQ,WAAW;IACzC,MAAM,SAAS,MAAM,QAAQ,YAAY,MAAM,EAC7C,qBAAqB,QAAQ,oBAC/B,CAAC;IACD,IAAI,QAAQ;KACV,MAAM,QAAQ,SAAS,WAAW;MAChC,IAAI,QAAQ;MACZ,WAAW,OAAO;MAClB,gBAAgB,OAAO;KACzB,CAAC;KACD,QAAQ,YAAY,OAAO;KAC3B,QAAQ,iBAAiB,OAAO;KAChC,UAAU,OAAO;KACjB,uBAAuB;IACzB;GACF;GAEA,MAAM,QAAQ,MAAM,mBAAmB;GAQvC,MAAM,UAAU,MAAM,qBAAqB,SAAS;GACpD,MAAM,aAAc,MAAM,mBAAmB,OAAO,oBAAoB,QAAQ,OAAO,OAAO,KAAM;GAEpG,MAAM,sBACJ,MAAM,cAAc,SAAS,EAAE,UAAU,WAAW,GAAG,KAAA,GAAW;IAChE,GAAI,iBAAiB,EAAE,kBAAkB,QAAQ,IAAI,CAAC;IACtD,cAAc;GAChB,CAAC;GACH,MAAM,kBAAkB,QAAmD,aACzE,gBAAgB;IACd,KAAK;KAAE,IAAI,QAAQ;KAAI,gBAAgB;KAAS,gBAAgB,QAAQ;IAAe;IACvF,UAAU;KAAgB;KAAc,eAAe,WAAW;IAAc;IAChF,SAAS;IACT;IACA,SAAS,QAAQ;IAIjB,4BAA4B;GAC9B,CAAC;GACH,MAAM,gBAAgB,UAAmB,iBAAiB,oBAAoB,MAAM,SAAS;GAE7F,IAAI,UAAU,MAAM,cAAc;GAKlC,IAAI,sBAAsB,MAAM,sBAAsB,SAAS,SAAS,WAAW,aAAa;GAIhG,MAAM,2BACJ,CAAC,CAAC,QAAQ,sBACV,CAAC,QAAQ,kBACT,CAAC,wBACA,MAAM,oBAAoB,SAAS,SAAS,YAAY;GAC3D,IAAI;IACF,MAAM,eAAe,SAAS,wBAAwB;GACxD,SAAS,OAAO;IACd,IAAI,CAAC,aAAa,KAAK,GAAG,MAAM;IAMhC,MAAM,MAAM,gBAAgB,SAAS,OAAO;IAC5C,UAAU,MAAM,cAAc;IAC9B,IAAI;KAGF,MAAM,eAAe,SAAS,KAAK;IACrC,SAAS,YAAY;KAGnB,IAAI,aAAa,UAAU,GAAG,MAAM,MAAM,gBAAgB,SAAS,OAAO;KAC1E,MAAM;IACR;GACF;GACA,MAAM,sBAAsB,SAAS,SAAS;IAC5C,QAAQ,QAAQ;IAChB,YAAY,QAAQ,cAAc,kBAAkB,UAAU,WAAW;IACzE;IACc;GAChB,CAAC;GAGD,IAAI,kBAAkB,gBAAgB,CAAC,0BACrC,IAAI;IACF,MAAM,iBAAiB,SAAS,SAAS,kBAAkB,YAAY;GACzE,SAAS,YAAY;IACnB,IAAI,kBAAkB,iBACpB,IAAI;KACF,MAAM,oBAAoB,SAAS,SAAS,kBAAkB,iBAAiB,EAC7E,WAAW,2BACb,CAAC;IACH,SAAS,eAAe;KACtB,QAAQ,KAAK,iEAAiE;MAC5E,OAAO,QAAQ;MACf,WAAW,QAAQ;MACnB,qBAAqB,QAAQ;MAC7B,OAAO,yBAAyB,QAAQ,cAAc,QAAQ,MAAM,IAAK,IAAI,OAAO,aAAa;KACnG,CAAC;IACH;IAEF,MAAM;GACR;GAGF,MAAM,oBAA6C;IACjD,SAAQ,eAAc;KACpB,IAAI,CAAC,QAAQ,wBACX,MAAM,IAAI,MAAM,4EAA4E;KAE9F,QAAQ,uBAAuB,YAAY,UAAU;KACrD,kBAAkB,UAAU;IAC9B;IACA;IACA,SAAS;IACT,YAAY;IACZ,yBAAyB;GAC3B;GAQA,IAAI,kBAAkB,WAAW,QAAQ,SAAS,MAAM,qBAAqB;IAC3E,IAAI;KACF,MAAM,MAAM,gBAAgB,SAAS,OAAO;IAC9C,SAAS,eAAe;KACtB,QAAQ,KAAK,iFAAiF;MAC5F,OAAO,QAAQ;MACf,WAAW,QAAQ;MACnB,OAAO,yBAAyB,QAAQ,cAAc,QAAQ,MAAM,IAAK,IAAI,OAAO,aAAa;KACnG,CAAC;IACH;IACA,MAAM,aAAa;GACrB;GACA,qBAAqB,IAAI,aAAa,iBAAiB;GACvD,2BAA2B,iBAAiB;GAC5C,OAAO;EACT;EAMA,MAAM,qBAAqB,YAAmC;GAC5D,MAAM,QAAQ,sBAAsB,IAAI,WAAW;GACnD,IAAI,OAAO,OAAO;GAClB,IAAI,WAAW,yBAAyB,IAAI,WAAW;GACvD,IAAI,CAAC,UAAU;IACb,WAAW,mBAAmB;IAC9B,yBAAyB,IAAI,aAAa,QAAQ;IAClD,SAAS,MACP,OAAM;KACJ,sBAAsB,IAAI,aAAa,EAAE;KAKzC,UAAe,QAAQ,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC;IACjD,SACM,CAAC,CACT;GACF;GACA,IAAI;IACF,OAAO,MAAM;GACf,UAAU;IACR,IAAI,yBAAyB,IAAI,WAAW,MAAM,UAChD,yBAAyB,OAAO,WAAW;GAE/C;EACF;EAMA,MAAM,cAAc;GAClB,IAAI,KAAK;IACP,OAAO,sBAAsB,IAAI,WAAW,CAAC,EAAE,MAAM;GACvD;GACA,MAAM;GACN,IAAI,WAAW;IACb,OAAO,MAAM;GACf;GACA,IAAI,SAAS;IACX,OAAO,sBAAsB,IAAI,WAAW,IAAI,UAAU;GAC5D;GACA,IAAI,sBAAsB;IACxB,OAAO,sBAAsB,IAAI,WAAW,CAAC,EAAE,uBAAuB;GACxE;GACA,kBAAkB;IAIhB,OAAO,sBAAsB,IAAI,WAAW,CAAC,EAAE,kBAAkB,KAAK,MAAM,gBAAgB;GAC9F;GACA,QAAe;IACb,MAAM,IAAI,MAAM,kEAAkE;GACpF;GACA,MAAM,QAAQ,CAMd;GACA,MAAM,UAAU;IAEd,QAAO,MADe,mBAAmB,EAAA,CAC1B,QAAQ;GACzB;GACA,MAAM,eAAe,SAAiB,MAAiB,SAAmC;IACxF,MAAM,UAAU,MAAM,mBAAmB;IACzC,IAAI;KACF,OAAO,MAAM,QAAQ,eAAe,SAAS,MAAM,OAAO;IAC5D,SAAS,OAAO;KACd,IAAI,CAAC,mBAAmB,KAAK,KAAK,EAAE,kBAAkB,sBAAsB,OAAO,OAAO,IAAI,MAAM;KAQpG,IAAI,sBAAsB,IAAI,WAAW,MAAM,SAC7C,sBAAsB,OAAO,WAAW;KAG1C,QAAO,MADe,mBAAmB,EAAA,CAC1B,eAAe,SAAS,MAAM,OAAO;IACtD;GACF;GACA,uBAAuB,MAAc,OAAe;IAClD,MAAM,UAAU,sBAAsB,IAAI,WAAW;IACrD,IAAI,CAAC,SAAS,wBACZ,MAAM,IAAI,MAAM,sDAAsD;IAExE,QAAQ,uBAAuB,MAAM,KAAK;GAC5C;GACA,MAAM,WAAW;IAEf,MAAM,sBAAsB,IAAI,WAAW,CAAC,EAAE,WAAW;GAC3D;GACA,MAAM,OAAO;IACX,MAAM,sBAAsB,IAAI,WAAW,CAAC,EAAE,OAAO;GACvD;EACF;EAEA,MAAM,aAAa,IAAI,kBAAkB;GACvC,IAAI,cAAc,YAAY,GAAG;GACjC,SAAS;GACT;EACF,CAAC;EACD,MAAM,oBAAoB;GAAC,KAAK,KAAK,WAAW,QAAQ;GAAG;GAAkB;EAAgB;EAC7F,MAAM,uBAAuB,IAAI,+BAA+B,kBAC9D,sBAAsB,IAAI,WAAW,CACvC;EAEA,MAAM,YAAY,IAAI,UAAU;GAC9B,IAAI;GACJ,MAAM;GACN;GACA,SAAS;GACT,OAAO;GACP,QAAQ,CAPU,GAAI,yBAAyB,SAAS,CAAC,GAAI,GAAG,iBAO/C;GAIjB,aACE,yBAAyB,aAAa,sBAAsB,iBAAiB,KAAK;EACtF,CAAC;EAOD,QAAQ,aAAa,WAAW,aAAa,EAAE,QAAQ,SAAS,CAAC;EAIjE,sBAAsB,IAAI,aAAa,SAAS;EAgBhD,IAAI,CAAC,MAXoB,kBAAkB,SACzC,QAAQ,WACR,aACA,qBACA,YAAY;GACV,qBAAqB,OAAO,WAAW;GACvC,sBAAsB,OAAO,WAAW;GACxC,sBAAsB,OAAO,WAAW;GACxC,MAAM,QAAQ,kBAAkB,WAAW;EAC7C,CACF,GAEE,MAAM,IAAI,MAAM,mBAAmB,QAAQ,UAAU,8CAA8C;EAMrG,IAAI,aACF,mBAAwB,CAAC,CAAC,OAAM,UAAS;GACvC,QAAQ,MAAM,sDAAsD,YAAY,UAAU,KAAK;EACjG,CAAC;EAEH,OAAO;CACT;AACF;AAEA,MAAa,sBAAsB,uBAAuB"}
@@ -40,7 +40,7 @@ Write the full plan into the conversation, structured as:
40
40
  - **Assumptions** — every recorded design decision and understanding correction from the run.
41
41
  - **Open questions** — only the decisions that genuinely need a human.
42
42
 
43
- The plan must be executable by someone with no access to this conversation beyond this message.
43
+ The plan must be executable by someone with no access to this conversation beyond this message. Write it to `.artifacts/plans/issue-<number>.md` and include the same plan in the conversation.
44
44
 
45
45
  ## Phase 4: Transition
46
46