stageflow 0.27.0 → 0.30.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 (261) hide show
  1. package/README.md +4 -0
  2. package/builtin-skills/browser/SKILL.md +42 -0
  3. package/dist/agent/claudeAdapter.js +2 -2
  4. package/dist/agent/cursorProvider.d.ts +2 -0
  5. package/dist/agent/cursorProvider.js +30 -5
  6. package/dist/agent/fakeAgent.d.ts +1 -0
  7. package/dist/agent/fakeAgent.js +13 -0
  8. package/dist/agent/piAdapter.d.ts +3 -24
  9. package/dist/agent/piAdapter.js +49 -47
  10. package/dist/agent/piSessionFactory.d.ts +38 -0
  11. package/dist/agent/piSessionFactory.js +60 -0
  12. package/dist/agent/port.d.ts +2 -0
  13. package/dist/agent/streamLogRedact.d.ts +6 -0
  14. package/dist/agent/streamLogRedact.js +23 -1
  15. package/dist/browser/anchor.d.ts +27 -0
  16. package/dist/browser/anchor.js +127 -0
  17. package/dist/browser/auditSink.d.ts +38 -0
  18. package/dist/browser/auditSink.js +34 -0
  19. package/dist/browser/browserHost.d.ts +86 -0
  20. package/dist/browser/browserHost.js +7 -0
  21. package/dist/browser/browserSweep.d.ts +14 -0
  22. package/dist/browser/browserSweep.js +54 -0
  23. package/dist/browser/browserTeardown.d.ts +56 -0
  24. package/dist/browser/browserTeardown.js +235 -0
  25. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
  26. package/dist/browser/fakeRemoteBrowserHost.js +34 -0
  27. package/dist/browser/gateHandoff.d.ts +25 -0
  28. package/dist/browser/gateHandoff.js +83 -0
  29. package/dist/browser/humanLogin.d.ts +18 -0
  30. package/dist/browser/humanLogin.js +34 -0
  31. package/dist/browser/keyProvider.d.ts +10 -0
  32. package/dist/browser/keyProvider.js +39 -0
  33. package/dist/browser/localBrowserHost.d.ts +10 -0
  34. package/dist/browser/localBrowserHost.js +152 -0
  35. package/dist/browser/localProfileLock.d.ts +2 -0
  36. package/dist/browser/localProfileLock.js +218 -0
  37. package/dist/browser/localProfileStore.d.ts +5 -0
  38. package/dist/browser/localProfileStore.js +91 -0
  39. package/dist/browser/loginCheck.d.ts +53 -0
  40. package/dist/browser/loginCheck.js +147 -0
  41. package/dist/browser/memoryProfileLock.d.ts +2 -0
  42. package/dist/browser/memoryProfileLock.js +53 -0
  43. package/dist/browser/memoryProfileStore.d.ts +2 -0
  44. package/dist/browser/memoryProfileStore.js +31 -0
  45. package/dist/browser/navigationAudit.d.ts +16 -0
  46. package/dist/browser/navigationAudit.js +53 -0
  47. package/dist/browser/persistedEnv.d.ts +3 -0
  48. package/dist/browser/persistedEnv.js +15 -0
  49. package/dist/browser/profileLock.d.ts +40 -0
  50. package/dist/browser/profileLock.js +10 -0
  51. package/dist/browser/profileStore.d.ts +23 -0
  52. package/dist/browser/profileStore.js +39 -0
  53. package/dist/browser/runLiveness.d.ts +13 -0
  54. package/dist/browser/runLiveness.js +39 -0
  55. package/dist/browser/sitePolicy.d.ts +10 -0
  56. package/dist/browser/sitePolicy.js +61 -0
  57. package/dist/browser/stageBrowserEnv.d.ts +22 -0
  58. package/dist/browser/stageBrowserEnv.js +147 -0
  59. package/dist/browser/stageProfileLock.d.ts +17 -0
  60. package/dist/browser/stageProfileLock.js +39 -0
  61. package/dist/cli/browserCommand.d.ts +44 -0
  62. package/dist/cli/browserCommand.js +472 -0
  63. package/dist/cli/hostClient.d.ts +20 -0
  64. package/dist/cli/hostClient.js +36 -2
  65. package/dist/cli/initCommand.d.ts +1 -0
  66. package/dist/cli/initCommand.js +23 -7
  67. package/dist/cli/initTemplates.d.ts +5 -2
  68. package/dist/cli/initTemplates.js +25 -2
  69. package/dist/cli/runsCommand.js +6 -2
  70. package/dist/cli/triggerCommand.d.ts +12 -0
  71. package/dist/cli/triggerCommand.js +183 -0
  72. package/dist/cli/validateCommand.d.ts +1 -1
  73. package/dist/cli/validateCommand.js +23 -6
  74. package/dist/cli.js +31 -2
  75. package/dist/config/browseCatalog.d.ts +1 -0
  76. package/dist/config/browseCatalog.js +5 -4
  77. package/dist/config/builtinSkills.d.ts +4 -0
  78. package/dist/config/builtinSkills.js +23 -0
  79. package/dist/config/catalogRelativePath.d.ts +1 -0
  80. package/dist/config/catalogRelativePath.js +10 -2
  81. package/dist/config/createTrigger.d.ts +27 -0
  82. package/dist/config/createTrigger.js +232 -0
  83. package/dist/config/draftPackage.d.ts +117 -0
  84. package/dist/config/draftPackage.js +550 -0
  85. package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
  86. package/dist/config/ensureCatalogScanRoot.js +73 -0
  87. package/dist/config/hostConfig.d.ts +2 -0
  88. package/dist/config/hostConfig.js +27 -0
  89. package/dist/config/listSkills.d.ts +2 -2
  90. package/dist/config/listSkills.js +17 -3
  91. package/dist/config/loadOutcome.d.ts +6 -0
  92. package/dist/config/loadPipeline.js +16 -0
  93. package/dist/config/loadStage.js +7 -0
  94. package/dist/config/loadStageflowManifest.js +25 -0
  95. package/dist/config/loadTrigger.d.ts +9 -0
  96. package/dist/config/loadTrigger.js +120 -0
  97. package/dist/config/multiProjectCatalog.d.ts +2 -0
  98. package/dist/config/multiProjectCatalog.js +26 -18
  99. package/dist/config/normalizePipelineStageEntry.js +8 -1
  100. package/dist/config/parseCompletionContract.js +6 -0
  101. package/dist/config/pipelineStageKeys.js +1 -0
  102. package/dist/config/publishDraftPackage.d.ts +7 -0
  103. package/dist/config/publishDraftPackage.js +25 -0
  104. package/dist/config/resolveCatalogContext.js +1 -1
  105. package/dist/config/resolvePipelineDag.js +1 -1
  106. package/dist/config/scanCatalogPaths.d.ts +1 -1
  107. package/dist/config/scanCatalogPaths.js +6 -2
  108. package/dist/config/stageBrowser.d.ts +12 -0
  109. package/dist/config/stageBrowser.js +118 -0
  110. package/dist/config/triggerCatalog.d.ts +10 -0
  111. package/dist/config/triggerCatalog.js +37 -0
  112. package/dist/config/validateCatalog.d.ts +13 -3
  113. package/dist/config/validateCatalog.js +83 -2
  114. package/dist/config/validateCompletionContract.js +8 -0
  115. package/dist/envelope/preEmitChecks.d.ts +1 -1
  116. package/dist/envelope/preEmitChecks.js +8 -0
  117. package/dist/logging/redact.d.ts +4 -0
  118. package/dist/logging/redact.js +17 -0
  119. package/dist/mcp/catalogTools.js +43 -8
  120. package/dist/mcp/controlTools.js +3 -1
  121. package/dist/mcp/deps.d.ts +2 -0
  122. package/dist/mcp/server.js +1 -0
  123. package/dist/mcp/tools.js +2 -0
  124. package/dist/mcp/triggerTools.d.ts +3 -0
  125. package/dist/mcp/triggerTools.js +176 -0
  126. package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
  127. package/dist/operatorAgent/docsRetrieval.js +269 -0
  128. package/dist/operatorAgent/draftContext.d.ts +16 -0
  129. package/dist/operatorAgent/draftContext.js +68 -0
  130. package/dist/operatorAgent/fakeHost.d.ts +39 -0
  131. package/dist/operatorAgent/fakeHost.js +92 -0
  132. package/dist/operatorAgent/host.d.ts +40 -0
  133. package/dist/operatorAgent/host.js +179 -0
  134. package/dist/operatorAgent/index.d.ts +9 -0
  135. package/dist/operatorAgent/index.js +8 -0
  136. package/dist/operatorAgent/piModel.d.ts +80 -0
  137. package/dist/operatorAgent/piModel.js +528 -0
  138. package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
  139. package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
  140. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
  141. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
  142. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
  143. package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
  144. package/dist/operatorAgent/proposals.d.ts +32 -0
  145. package/dist/operatorAgent/proposals.js +144 -0
  146. package/dist/operatorAgent/types.d.ts +128 -0
  147. package/dist/operatorAgent/types.js +1 -0
  148. package/dist/package-meta.d.ts +1 -1
  149. package/dist/package-meta.js +1 -1
  150. package/dist/project/findManifestRoot.d.ts +3 -0
  151. package/dist/project/findManifestRoot.js +35 -0
  152. package/dist/project/resolveStageflowContext.d.ts +7 -0
  153. package/dist/project/resolveStageflowContext.js +44 -28
  154. package/dist/runstore/assertTmpdir.js +6 -2
  155. package/dist/runstore/port.d.ts +28 -0
  156. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
  157. package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
  158. package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
  159. package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
  160. package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
  161. package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
  162. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
  163. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
  164. package/dist/runstore/sqlite/migrations/index.js +6 -0
  165. package/dist/runtime/answerResume.d.ts +1 -0
  166. package/dist/runtime/answerResume.js +3 -1
  167. package/dist/runtime/completionCheckRunner.d.ts +12 -1
  168. package/dist/runtime/completionCheckRunner.js +22 -0
  169. package/dist/runtime/credentialBinding.d.ts +2 -0
  170. package/dist/runtime/credentialBinding.js +3 -0
  171. package/dist/runtime/emailSource.d.ts +72 -0
  172. package/dist/runtime/emailSource.js +304 -0
  173. package/dist/runtime/githubPollSource.d.ts +86 -0
  174. package/dist/runtime/githubPollSource.js +284 -0
  175. package/dist/runtime/pipelineRunner.d.ts +4 -0
  176. package/dist/runtime/pipelineRunner.js +3 -0
  177. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  178. package/dist/runtime/pipelineScheduler.js +85 -1
  179. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  180. package/dist/runtime/runManager.d.ts +11 -0
  181. package/dist/runtime/runManager.js +149 -1
  182. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  183. package/dist/runtime/runRetryCoordinator.js +1 -0
  184. package/dist/runtime/scheduleSource.d.ts +38 -0
  185. package/dist/runtime/scheduleSource.js +119 -0
  186. package/dist/runtime/settingsFile.d.ts +5 -0
  187. package/dist/runtime/settingsFile.js +61 -54
  188. package/dist/runtime/stageAttemptBootstrap.js +55 -3
  189. package/dist/runtime/stageEnvironment.js +1 -0
  190. package/dist/runtime/stageHitl.d.ts +1 -0
  191. package/dist/runtime/stageHitl.js +5 -1
  192. package/dist/runtime/stageProcessLauncher.d.ts +2 -0
  193. package/dist/runtime/stageProcessLauncher.js +44 -5
  194. package/dist/runtime/stageRunner.d.ts +5 -0
  195. package/dist/runtime/stageRunner.js +30 -6
  196. package/dist/runtime/stageStreamLog.d.ts +1 -0
  197. package/dist/runtime/stageStreamLog.js +2 -1
  198. package/dist/runtime/triggerCatalog.d.ts +6 -0
  199. package/dist/runtime/triggerCatalog.js +24 -0
  200. package/dist/runtime/triggerEventMatch.d.ts +2 -0
  201. package/dist/runtime/triggerEventMatch.js +6 -0
  202. package/dist/runtime/triggerPort.d.ts +14 -0
  203. package/dist/runtime/triggerPort.js +1 -0
  204. package/dist/runtime/triggerRunner.d.ts +17 -0
  205. package/dist/runtime/triggerRunner.js +89 -0
  206. package/dist/runtime/verifiedStageExecution.d.ts +7 -0
  207. package/dist/runtime/verifiedStageExecution.js +14 -0
  208. package/dist/runtime/webhookSignature.d.ts +1 -0
  209. package/dist/runtime/webhookSignature.js +12 -0
  210. package/dist/server/bootstrap.d.ts +6 -0
  211. package/dist/server/bootstrap.js +64 -0
  212. package/dist/server/controlToken.js +13 -0
  213. package/dist/server/createHttpHost.js +3 -0
  214. package/dist/server/healthSurfaces.js +31 -6
  215. package/dist/server/http.d.ts +15 -0
  216. package/dist/server/http.js +1251 -74
  217. package/dist/server/requestAuthContext.d.ts +1 -1
  218. package/dist/server/triggerRoutes.d.ts +15 -0
  219. package/dist/server/triggerRoutes.js +268 -0
  220. package/dist/tools/askOperator.d.ts +3 -2
  221. package/dist/types/completion.d.ts +3 -0
  222. package/dist/types/pipeline.d.ts +3 -1
  223. package/dist/types/preEmitCheck.d.ts +7 -0
  224. package/dist/types/stage.d.ts +14 -0
  225. package/dist/types/stageflowManifest.d.ts +3 -0
  226. package/dist/types/trigger.d.ts +18 -0
  227. package/dist/types/trigger.js +1 -0
  228. package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
  229. package/dist/ui/assets/index-CntB4bZR.js +133 -0
  230. package/dist/ui/index.html +2 -2
  231. package/dist/workshop/autosave.d.ts +58 -0
  232. package/dist/workshop/autosave.js +222 -0
  233. package/dist/workshop/buildStore.d.ts +79 -0
  234. package/dist/workshop/buildStore.js +326 -0
  235. package/dist/workshop/chatTurn.d.ts +232 -0
  236. package/dist/workshop/chatTurn.js +475 -0
  237. package/dist/workshop/modelSettings.d.ts +18 -0
  238. package/dist/workshop/modelSettings.js +44 -0
  239. package/dist/workshop/sessionStore.d.ts +61 -0
  240. package/dist/workshop/sessionStore.js +254 -0
  241. package/dist/workshop/workshopPin.d.ts +72 -0
  242. package/dist/workshop/workshopPin.js +193 -0
  243. package/examples/README.md +2 -1
  244. package/examples/browser-session/README.md +77 -0
  245. package/examples/browser-session/browser-session.pipeline.yaml +50 -0
  246. package/examples/browser-session/browser-session.task.yaml +6 -0
  247. package/examples/browser-session/check-login.yaml +22 -0
  248. package/examples/browser-session/fixture-server.mjs +64 -0
  249. package/examples/browser-session/human-login.yaml +31 -0
  250. package/examples/browser-session/merge.yaml +36 -0
  251. package/examples/browser-session/work-a.yaml +41 -0
  252. package/examples/browser-session/work-b.yaml +41 -0
  253. package/examples/stage-mcp/README.md +3 -2
  254. package/examples/stageflow.yaml +51 -0
  255. package/package.json +5 -1
  256. package/skills/stageflow/references/control-surface.md +3 -1
  257. package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
  258. package/skills/stageflow-run/SKILL.md +2 -0
  259. package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
  260. package/skills/stageflow-setup/SKILL.md +11 -0
  261. package/dist/ui/assets/index-B6hlSXXn.js +0 -120
@@ -17,10 +17,17 @@ import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnaps
17
17
  import { InlinePipelineTooLargeError, PipelinePreflightError, PipelineValidationError, QueuedRunActivationAborted, startPipeline, } from "./pipelineRunner.js";
18
18
  import { INLINE_PIPELINE_TOO_LARGE, pipelineBodyBytes, pipelinePersistenceForStart, } from "./startPayload.js";
19
19
  import { materializeRunSkills, validateSkillsPayload, } from "./runSkills.js";
20
- import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, runPipelineDag, } from "./pipelineScheduler.js";
20
+ import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, retryRun, runPipelineDag, } from "./pipelineScheduler.js";
21
21
  import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
22
22
  import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
23
23
  import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
24
+ import { isHumanLoginStage } from "../browser/humanLogin.js";
25
+ import { defaultStageBrowserSupport, resolveStageBrowserEnv, } from "../browser/stageBrowserEnv.js";
26
+ import { teardownRunBrowsers, teardownStageBrowser, } from "../browser/browserTeardown.js";
27
+ import { createRunLiveness } from "../browser/runLiveness.js";
28
+ import { acquireStageProfile, stageProfileLock, } from "../browser/stageProfileLock.js";
29
+ import { sweepOrphanBrowserSessions } from "../browser/browserSweep.js";
30
+ import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
24
31
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
25
32
  import { logger as rootLogger } from "../logging/logger.js";
26
33
  import { registerNamedSecrets } from "../logging/namedSecrets.js";
@@ -584,6 +591,23 @@ export class RunManager {
584
591
  .getActiveStageProcesses()
585
592
  .some((entry) => entry.runId === runId && entry.stageId === stageId);
586
593
  }
594
+ async sweepBrowserSessions() {
595
+ const support = this.options.browser ?? defaultStageBrowserSupport();
596
+ const isRunLive = createRunLiveness(this.options.store);
597
+ await stageProfileLock(support)
598
+ .reclaimStale(isRunLive)
599
+ .catch(() => 0);
600
+ return sweepOrphanBrowserSessions({
601
+ isRunLive,
602
+ ...(support.runner !== undefined ? { runner: support.runner } : {}),
603
+ ...(support.closeWaitMs !== undefined
604
+ ? { closeWaitMs: support.closeWaitMs }
605
+ : {}),
606
+ ...(support.socketRoot !== undefined
607
+ ? { socketRoot: support.socketRoot }
608
+ : {}),
609
+ });
610
+ }
587
611
  async reconcileOrphanedStages() {
588
612
  const reconciled = [];
589
613
  const runs = await this.options.store.listRuns();
@@ -847,6 +871,12 @@ export class RunManager {
847
871
  await this.stageProcessLauncher.cancelRun(runId);
848
872
  }
849
873
  }
874
+ await teardownStageBrowser(this.options.browser ?? defaultStageBrowserSupport(), {
875
+ runId,
876
+ runDir: this.options.store.getWorkspaceDir(runId),
877
+ stageId,
878
+ events: () => this.options.store.listStageEvents(runId, stageId),
879
+ }).catch(() => undefined);
850
880
  await markStageInterrupted({
851
881
  store: this.options.store,
852
882
  runId,
@@ -861,6 +891,14 @@ export class RunManager {
861
891
  await syncRunStatusFromStages(this.options.store, runId);
862
892
  const after = await this.options.store.readRun(runId);
863
893
  const hasWaiting = after.stages.some((s) => s.status === "waiting_for_input");
894
+ if (!hasWaiting &&
895
+ !(await createRunLiveness(this.options.store)(runId).catch(() => true))) {
896
+ await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
897
+ runId,
898
+ runDir: this.options.store.getWorkspaceDir(runId),
899
+ events: (stageId) => this.options.store.listStageEvents(runId, stageId),
900
+ }).catch(() => undefined);
901
+ }
864
902
  if (!hasWaiting && this.active.has(runId)) {
865
903
  this.removeActiveEntry(runId, false);
866
904
  }
@@ -912,6 +950,11 @@ export class RunManager {
912
950
  if (this.stageProcessLauncher !== undefined) {
913
951
  await this.stageProcessLauncher.cancelRun(runId);
914
952
  }
953
+ await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
954
+ runId,
955
+ runDir: this.options.store.getWorkspaceDir(runId),
956
+ events: (stageId) => this.options.store.listStageEvents(runId, stageId),
957
+ });
915
958
  const detail = await this.options.store.readRun(runId);
916
959
  for (const stage of detail.stages) {
917
960
  if (stage.status !== "pending" &&
@@ -1478,6 +1521,9 @@ export class RunManager {
1478
1521
  agent: this.options.agent,
1479
1522
  cwd: meta.project_root ?? this.projectRoot,
1480
1523
  operatorCatalog: this.options.operatorCatalog,
1524
+ ...(this.options.browser !== undefined
1525
+ ? { browser: this.options.browser }
1526
+ : {}),
1481
1527
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
1482
1528
  executionMode: this.executionMode,
1483
1529
  stageProcessLauncher: this.stageProcessLauncher,
@@ -1841,6 +1887,9 @@ export class RunManager {
1841
1887
  checkoutRoot: meta.checkout_root,
1842
1888
  hitl: this.hitl,
1843
1889
  operatorCatalog: this.options.operatorCatalog,
1890
+ ...(this.options.browser !== undefined
1891
+ ? { browser: this.options.browser }
1892
+ : {}),
1844
1893
  },
1845
1894
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
1846
1895
  executionMode: this.executionMode,
@@ -1897,10 +1946,33 @@ export class RunManager {
1897
1946
  runWorkspaceDir: workspaceDir,
1898
1947
  hostEnv: process.env,
1899
1948
  });
1949
+ const resumedStage = loaded.stages.find((s) => s.id === definitionIdForInstance(meta.pipeline_dag, stageId));
1950
+ if (resumedStage?.browser?.profile !== undefined) {
1951
+ await acquireStageProfile(this.options.browser ?? defaultStageBrowserSupport(), {
1952
+ profile: resumedStage.browser.profile,
1953
+ owner: { runId, stageId },
1954
+ isRunLive: createRunLiveness(store),
1955
+ halted: () => true,
1956
+ }).then((outcome) => {
1957
+ if (outcome === "halted") {
1958
+ throw new Error(`browser profile "${resumedStage.browser?.profile}" is held by another run`);
1959
+ }
1960
+ });
1961
+ }
1962
+ const browserEnv = await resolveStageBrowserEnv(this.options.browser ?? defaultStageBrowserSupport(), {
1963
+ runId,
1964
+ stageId,
1965
+ runDir: workspaceDir,
1966
+ browser: resumedStage?.browser,
1967
+ attempt,
1968
+ humanLogin: isHumanLoginStage(meta.pipeline_dag, definitionIdForInstance(meta.pipeline_dag, stageId), resumedStage?.browser),
1969
+ resuming: true,
1970
+ });
1900
1971
  const launchResult = await launcher.launch({
1901
1972
  runId,
1902
1973
  stageId,
1903
1974
  rootDir: runProjectRoot,
1975
+ ...(browserEnv !== undefined ? { browserEnv } : {}),
1904
1976
  mode: "resume",
1905
1977
  resumeAnswer: opaqueAnswer,
1906
1978
  attempt,
@@ -1914,12 +1986,53 @@ export class RunManager {
1914
1986
  if (launchResult.type === "waiting") {
1915
1987
  return { ok: true };
1916
1988
  }
1989
+ await teardownStageBrowser(this.options.browser ?? defaultStageBrowserSupport(), {
1990
+ runId,
1991
+ runDir: workspaceDir,
1992
+ stageId,
1993
+ events: () => store.listStageEvents(runId, stageId),
1994
+ }).catch(() => undefined);
1917
1995
  if (launchResult.type === "failed") {
1996
+ const nextRepairAttempt = await this.scheduleRepairAfterVerifyFailure(runId, stageId, meta.pipeline_dag);
1997
+ if (nextRepairAttempt !== undefined) {
1998
+ await store.updateRunStatus(runId, "running");
1999
+ const repaired = await retryRun({
2000
+ prepared: {
2001
+ task,
2002
+ loaded,
2003
+ run: { runId, workspaceDir: store.getWorkspaceDir(runId) },
2004
+ agent: this.options.agent,
2005
+ store,
2006
+ cwd: meta.project_root ?? this.projectRoot,
2007
+ projectRoot: meta.project_root ?? this.projectRoot,
2008
+ checkoutRoot: meta.checkout_root,
2009
+ hitl: this.hitl,
2010
+ operatorCatalog: this.options.operatorCatalog,
2011
+ ...(this.options.browser !== undefined
2012
+ ? { browser: this.options.browser }
2013
+ : {}),
2014
+ },
2015
+ retryRoots: new Map([[stageId, nextRepairAttempt]]),
2016
+ maxActiveStagesPerRun: this.maxActiveStagesPerRun,
2017
+ executionMode: this.executionMode,
2018
+ stageProcessLauncher: launcher,
2019
+ schedulingHalt: this.ensureSchedulingHalt(runId),
2020
+ });
2021
+ if (repaired.outcome === "failed") {
2022
+ return { ok: false, reason: repaired.reason };
2023
+ }
2024
+ return { ok: true, verification: "failed_retrying" };
2025
+ }
1918
2026
  await store.appendStageEvent(runId, stageId, {
1919
2027
  event: "failed",
1920
2028
  reason: launchResult.reason,
1921
2029
  }, eventOptions);
1922
2030
  await store.updateRunStatus(runId, "failed");
2031
+ await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
2032
+ runId,
2033
+ runDir: workspaceDir,
2034
+ events: (stageId) => store.listStageEvents(runId, stageId),
2035
+ }).catch(() => undefined);
1923
2036
  return { ok: false, reason: launchResult.reason };
1924
2037
  }
1925
2038
  let initialPrior = null;
@@ -1941,6 +2054,9 @@ export class RunManager {
1941
2054
  checkoutRoot: meta.checkout_root,
1942
2055
  hitl: this.hitl,
1943
2056
  operatorCatalog: this.options.operatorCatalog,
2057
+ ...(this.options.browser !== undefined
2058
+ ? { browser: this.options.browser }
2059
+ : {}),
1944
2060
  },
1945
2061
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
1946
2062
  resumeFromStageId: stageId,
@@ -1964,6 +2080,11 @@ export class RunManager {
1964
2080
  reason,
1965
2081
  }, eventOptions);
1966
2082
  await store.updateRunStatus(runId, "failed");
2083
+ await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
2084
+ runId,
2085
+ runDir: store.getWorkspaceDir(runId),
2086
+ events: (stageId) => store.listStageEvents(runId, stageId),
2087
+ }).catch(() => undefined);
1967
2088
  }
1968
2089
  catch {
1969
2090
  // ignore secondary failures
@@ -1974,6 +2095,24 @@ export class RunManager {
1974
2095
  this.attachedWaiting.delete(`${runId}\0${stageId}`);
1975
2096
  }
1976
2097
  }
2098
+ /**
2099
+ * A resumed stage whose after-phase verification failed is repaired by the
2100
+ * same policy the scheduler applies to a first-launch failure. Returns the
2101
+ * freshly created attempt, or undefined when the failure is final.
2102
+ */
2103
+ async scheduleRepairAfterVerifyFailure(runId, stageId, dag) {
2104
+ const store = this.options.store;
2105
+ const recovery = dag?.nodes.find((node) => node.id === stageId)?.recovery;
2106
+ if (recovery?.mode !== "repair")
2107
+ return undefined;
2108
+ const latest = await store.getLatestStageExecution(runId, stageId);
2109
+ if (latest?.verification_outcome !== "failed")
2110
+ return undefined;
2111
+ const attempts = await store.countStageAttempts(runId, stageId);
2112
+ if (attempts >= recovery.max_attempts)
2113
+ return undefined;
2114
+ return (await store.createStageExecution(runId, stageId)).attempt;
2115
+ }
1977
2116
  async reconstructAndContinue(runId, stageId, opaqueAnswer) {
1978
2117
  try {
1979
2118
  return await resumeReconstructAndContinue({
@@ -2248,6 +2387,9 @@ export class RunManager {
2248
2387
  const schedulingHalt = this.ensureSchedulingHalt(materialized.runId);
2249
2388
  const started = await startPipeline({
2250
2389
  submission,
2390
+ ...(this.options.browser !== undefined
2391
+ ? { browser: this.options.browser }
2392
+ : {}),
2251
2393
  agent: this.options.agent,
2252
2394
  store: this.options.store,
2253
2395
  taskYaml,
@@ -2738,6 +2880,9 @@ export class RunManager {
2738
2880
  const schedulingHalt = this.ensureSchedulingHalt(runId);
2739
2881
  const started = await startPipeline({
2740
2882
  submission,
2883
+ ...(this.options.browser !== undefined
2884
+ ? { browser: this.options.browser }
2885
+ : {}),
2741
2886
  agent: this.options.agent,
2742
2887
  store: this.options.store,
2743
2888
  taskYaml,
@@ -2842,6 +2987,9 @@ export class RunManager {
2842
2987
  checkoutRoot: loadedMeta.checkout_root,
2843
2988
  hitl: this.hitl,
2844
2989
  operatorCatalog: this.options.operatorCatalog,
2990
+ ...(this.options.browser !== undefined
2991
+ ? { browser: this.options.browser }
2992
+ : {}),
2845
2993
  },
2846
2994
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
2847
2995
  executionMode: this.executionMode,
@@ -3,6 +3,7 @@ import { type RunDetail, type RunPipelineDagSnapshot, type RunStatus, type RunSt
3
3
  import type { PipelineRunResult } from "./pipelineRunner.js";
4
4
  import { type SchedulerPreparedPipeline } from "./pipelineScheduler.js";
5
5
  import type { StageExecutionMode } from "./stageConcurrency.js";
6
+ import type { StageBrowserSupport } from "../browser/browserHost.js";
6
7
  import type { StageProcessLauncher } from "./stageProcessLauncher.js";
7
8
  import type { StageHitlController } from "./stageHitl.js";
8
9
  import type { OperatorCatalog } from "./stageAttemptBootstrap.js";
@@ -58,6 +59,7 @@ export type RetryStageRequest = {
58
59
  agent: AgentPort;
59
60
  cwd: string;
60
61
  operatorCatalog?: OperatorCatalog;
62
+ browser?: StageBrowserSupport;
61
63
  maxActiveStagesPerRun: number;
62
64
  executionMode: StageExecutionMode;
63
65
  stageProcessLauncher?: StageProcessLauncher;
@@ -344,6 +344,7 @@ export class RunRetryCoordinator {
344
344
  checkoutRoot: meta.checkout_root,
345
345
  hitl: req.hitl,
346
346
  operatorCatalog: req.operatorCatalog,
347
+ ...(req.browser !== undefined ? { browser: req.browser } : {}),
347
348
  };
348
349
  if (this.isActive(runId)) {
349
350
  await this.addRoot(runId, stageId, execution.attempt);
@@ -0,0 +1,38 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { TriggerFireEvent, TriggerSourcePort } from "./triggerPort.js";
3
+ export declare const DEFAULT_TRIGGER_TICK_INTERVAL_MS = 30000;
4
+ /** Parse `STAGEFLOW_TRIGGER_TICK_INTERVAL_MS`; default 30s; `0` disables. */
5
+ export declare function triggerTickIntervalMsFromEnv(env?: NodeJS.ProcessEnv): number;
6
+ export type ScheduleSourceOptions = {
7
+ store: RunStore;
8
+ cwd?: string;
9
+ intervalMs?: number;
10
+ /** Injectable clock so tests can drive `tick()` without real timers. */
11
+ now?: () => Date;
12
+ logError?: (message: string) => void;
13
+ };
14
+ /**
15
+ * `TriggerSourcePort` adapter for `schedule`-kind triggers. Ticks on an
16
+ * interval; each tick re-scans the catalog for enabled `schedule` triggers,
17
+ * seeds `next_run_at` for any that don't have one yet (without firing), and
18
+ * fires any trigger whose `next_run_at` has passed — always recomputing the
19
+ * next fire time from *now*, never from the stale `next_run_at`, so a tick
20
+ * never backfills more than one missed occurrence.
21
+ */
22
+ export declare class ScheduleSource implements TriggerSourcePort {
23
+ private readonly store;
24
+ private readonly cwd;
25
+ private readonly intervalMs;
26
+ private readonly now;
27
+ private readonly logError;
28
+ private interval;
29
+ private inFlight;
30
+ constructor(options: ScheduleSourceOptions);
31
+ start(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
32
+ stop(): Promise<void>;
33
+ /**
34
+ * One discovery + seed + fire pass. Public so tests can drive it directly
35
+ * against an injected clock instead of waiting on real interval timers.
36
+ */
37
+ tick(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
38
+ }
@@ -0,0 +1,119 @@
1
+ import path from "node:path";
2
+ import { Cron } from "croner";
3
+ import { getCatalogScanPaths } from "../config/browseCatalog.js";
4
+ import { loadTriggerOutcome } from "../config/loadTrigger.js";
5
+ import { catalogContextFromStageflow } from "../config/resolveCatalogContext.js";
6
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
7
+ export const DEFAULT_TRIGGER_TICK_INTERVAL_MS = 30_000;
8
+ /** Parse `STAGEFLOW_TRIGGER_TICK_INTERVAL_MS`; default 30s; `0` disables. */
9
+ export function triggerTickIntervalMsFromEnv(env = process.env) {
10
+ const raw = env.STAGEFLOW_TRIGGER_TICK_INTERVAL_MS;
11
+ if (raw === undefined || raw.trim() === "")
12
+ return DEFAULT_TRIGGER_TICK_INTERVAL_MS;
13
+ const parsed = Number(raw);
14
+ if (!Number.isFinite(parsed) || parsed < 0)
15
+ return DEFAULT_TRIGGER_TICK_INTERVAL_MS;
16
+ return parsed;
17
+ }
18
+ function computeNextRunAt(schedule, from) {
19
+ const job = new Cron(schedule.cron, schedule.timezone !== undefined ? { timezone: schedule.timezone } : {});
20
+ return job.nextRun(from);
21
+ }
22
+ /**
23
+ * `TriggerSourcePort` adapter for `schedule`-kind triggers. Ticks on an
24
+ * interval; each tick re-scans the catalog for enabled `schedule` triggers,
25
+ * seeds `next_run_at` for any that don't have one yet (without firing), and
26
+ * fires any trigger whose `next_run_at` has passed — always recomputing the
27
+ * next fire time from *now*, never from the stale `next_run_at`, so a tick
28
+ * never backfills more than one missed occurrence.
29
+ */
30
+ export class ScheduleSource {
31
+ store;
32
+ cwd;
33
+ intervalMs;
34
+ now;
35
+ logError;
36
+ interval;
37
+ inFlight = false;
38
+ constructor(options) {
39
+ this.store = options.store;
40
+ this.cwd = options.cwd ?? process.cwd();
41
+ this.intervalMs = options.intervalMs ?? DEFAULT_TRIGGER_TICK_INTERVAL_MS;
42
+ this.now = options.now ?? (() => new Date());
43
+ this.logError =
44
+ options.logError ??
45
+ ((message) => {
46
+ console.error(message);
47
+ });
48
+ }
49
+ async start(onFire) {
50
+ if (this.intervalMs <= 0)
51
+ return;
52
+ this.interval = setInterval(() => {
53
+ if (this.inFlight)
54
+ return;
55
+ this.inFlight = true;
56
+ void this.tick(onFire)
57
+ .catch((err) => {
58
+ this.logError(`trigger schedule tick failed: ${err instanceof Error ? err.message : String(err)}`);
59
+ })
60
+ .finally(() => {
61
+ this.inFlight = false;
62
+ });
63
+ }, this.intervalMs).unref();
64
+ }
65
+ async stop() {
66
+ if (this.interval !== undefined)
67
+ clearInterval(this.interval);
68
+ this.interval = undefined;
69
+ }
70
+ /**
71
+ * One discovery + seed + fire pass. Public so tests can drive it directly
72
+ * against an injected clock instead of waiting on real interval timers.
73
+ */
74
+ async tick(onFire) {
75
+ const ctx = catalogContextFromStageflow(await resolveStageflowContext(this.cwd));
76
+ const scanPaths = await getCatalogScanPaths(ctx);
77
+ if (!scanPaths)
78
+ return;
79
+ const now = this.now();
80
+ const projectRoot = ctx.projectRoot ?? undefined;
81
+ for (const triggerPath of scanPaths.triggerPaths) {
82
+ const outcome = await loadTriggerOutcome(triggerPath);
83
+ if (!outcome.ok)
84
+ continue;
85
+ const definition = outcome.value;
86
+ if (definition.kind !== "schedule" || !definition.enabled || !definition.schedule) {
87
+ continue;
88
+ }
89
+ let next;
90
+ try {
91
+ next = computeNextRunAt(definition.schedule, now);
92
+ }
93
+ catch (err) {
94
+ this.logError(`invalid schedule for trigger "${definition.id}": ${err instanceof Error ? err.message : String(err)}`);
95
+ continue;
96
+ }
97
+ const definitionRef = projectRoot !== undefined
98
+ ? path.relative(projectRoot, triggerPath).replace(/\\/g, "/")
99
+ : triggerPath;
100
+ await this.store.upsertTrigger({
101
+ id: definition.id,
102
+ definitionRef,
103
+ enabled: true,
104
+ });
105
+ const record = await this.store.getTrigger(definition.id);
106
+ if (!record?.next_run_at) {
107
+ if (next)
108
+ await this.store.setTriggerNextRun(definition.id, next.toISOString());
109
+ continue;
110
+ }
111
+ const dueAt = new Date(record.next_run_at);
112
+ if (dueAt.getTime() <= now.getTime()) {
113
+ await onFire({ triggerId: definition.id });
114
+ if (next)
115
+ await this.store.setTriggerNextRun(definition.id, next.toISOString());
116
+ }
117
+ }
118
+ }
119
+ }
@@ -3,16 +3,19 @@ import type { StageflowContext } from "../project/resolveStageflowContext.js";
3
3
  export declare const SETTINGS_FILE_NAME = "settings.json";
4
4
  export declare const INVALID_SLOT_COUNT_MESSAGE = "maxConcurrent must be an integer >= 1";
5
5
  export declare const INVALID_CREDENTIAL_SOURCE_MESSAGE = "credentialSource must be \"pi_home\" or \"sf_owned\"";
6
+ export declare const INVALID_WORKSHOP_MODEL_MESSAGE = "workshopModel must be a non-empty string";
6
7
  export type CredentialSource = "pi_home" | "sf_owned";
7
8
  export type FactorySettings = {
8
9
  maxConcurrent?: number;
9
10
  credentialSource?: CredentialSource;
11
+ workshopModel?: string;
10
12
  };
11
13
  export declare function globalSettingsFilePath(): string;
12
14
  export declare function settingsFilePathForContext(ctx: ProjectContext | StageflowContext): string;
13
15
  export declare function settingsFilePath(cwd: string): string;
14
16
  export declare function parseSlotCount(value: unknown): number | undefined;
15
17
  export declare function parseCredentialSource(value: unknown): CredentialSource | undefined;
18
+ export declare function parseWorkshopModelSetting(value: unknown): string | undefined;
16
19
  export declare function readCredentialSourceFromContext(ctx: ProjectContext): CredentialSource | undefined;
17
20
  export declare function readPersistedCredentialSourceFromContext(ctx: ProjectContext): CredentialSource | undefined;
18
21
  export declare function readFactorySettingsForContext(ctx: ProjectContext): FactorySettings;
@@ -25,6 +28,8 @@ export declare function writeMaxConcurrentToContext(ctx: ProjectContext, maxConc
25
28
  export declare function writeMaxConcurrentToFile(cwd: string, maxConcurrent: number): void;
26
29
  export declare function readMaxConcurrentFromGlobal(): number | undefined;
27
30
  export declare function writeMaxConcurrentToGlobal(maxConcurrent: number): void;
31
+ export declare function readWorkshopModelFromFile(cwd: string): string | undefined;
32
+ export declare function writeWorkshopModelToFile(cwd: string, workshopModel: string): void;
28
33
  export declare function readCredentialSourceFromFile(cwd: string): CredentialSource | undefined;
29
34
  export declare function writeCredentialSourceToContext(ctx: ProjectContext, credentialSource: CredentialSource): void;
30
35
  export declare function writeCredentialSourceToFile(cwd: string, credentialSource: CredentialSource): void;
@@ -6,6 +6,7 @@ import { storeRootFor } from "../runstore/paths.js";
6
6
  export const SETTINGS_FILE_NAME = "settings.json";
7
7
  export const INVALID_SLOT_COUNT_MESSAGE = "maxConcurrent must be an integer >= 1";
8
8
  export const INVALID_CREDENTIAL_SOURCE_MESSAGE = 'credentialSource must be "pi_home" or "sf_owned"';
9
+ export const INVALID_WORKSHOP_MODEL_MESSAGE = "workshopModel must be a non-empty string";
9
10
  export function globalSettingsFilePath() {
10
11
  return path.join(globalStageflowHome(), SETTINGS_FILE_NAME);
11
12
  }
@@ -33,6 +34,12 @@ export function parseCredentialSource(value) {
33
34
  }
34
35
  return undefined;
35
36
  }
37
+ export function parseWorkshopModelSetting(value) {
38
+ if (typeof value !== "string")
39
+ return undefined;
40
+ const trimmed = value.trim();
41
+ return trimmed.length > 0 ? trimmed : undefined;
42
+ }
36
43
  function readRawSettingsFromFile(filePath) {
37
44
  if (!existsSync(filePath))
38
45
  return {};
@@ -60,8 +67,50 @@ function parseFactorySettingsFromRaw(raw) {
60
67
  if (credentialSource !== undefined) {
61
68
  settings.credentialSource = credentialSource;
62
69
  }
70
+ const workshopModel = parseWorkshopModelSetting(raw.workshopModel);
71
+ if (workshopModel !== undefined) {
72
+ settings.workshopModel = workshopModel;
73
+ }
63
74
  return settings;
64
75
  }
76
+ function serializeFactorySettings(next) {
77
+ const out = {};
78
+ if (next.maxConcurrent !== undefined) {
79
+ out.maxConcurrent = next.maxConcurrent;
80
+ }
81
+ if (next.credentialSource !== undefined) {
82
+ out.credentialSource = next.credentialSource;
83
+ }
84
+ if (next.workshopModel !== undefined) {
85
+ out.workshopModel = next.workshopModel;
86
+ }
87
+ return out;
88
+ }
89
+ function applyFactorySettingsPatch(current, patch) {
90
+ if (patch.credentialSource !== undefined &&
91
+ parseCredentialSource(patch.credentialSource) === undefined) {
92
+ throw new Error(INVALID_CREDENTIAL_SOURCE_MESSAGE);
93
+ }
94
+ if (patch.maxConcurrent !== undefined &&
95
+ parseSlotCount(patch.maxConcurrent) === undefined) {
96
+ throw new Error(INVALID_SLOT_COUNT_MESSAGE);
97
+ }
98
+ if (patch.workshopModel !== undefined &&
99
+ parseWorkshopModelSetting(patch.workshopModel) === undefined) {
100
+ throw new Error(INVALID_WORKSHOP_MODEL_MESSAGE);
101
+ }
102
+ const next = { ...current };
103
+ if (patch.maxConcurrent !== undefined) {
104
+ next.maxConcurrent = patch.maxConcurrent;
105
+ }
106
+ if (patch.credentialSource !== undefined) {
107
+ next.credentialSource = patch.credentialSource;
108
+ }
109
+ if (patch.workshopModel !== undefined) {
110
+ next.workshopModel = parseWorkshopModelSetting(patch.workshopModel);
111
+ }
112
+ return next;
113
+ }
65
114
  export function readCredentialSourceFromContext(ctx) {
66
115
  if (ctx.isGitProject) {
67
116
  const fromProject = parseCredentialSource(readRawSettingsFromFile(settingsFilePathForContext(ctx)).credentialSource);
@@ -84,56 +133,14 @@ function ensureSettingsDirForContext(ctx) {
84
133
  mkdirSync(path.dirname(settingsFilePathForContext(ctx)), { recursive: true });
85
134
  }
86
135
  export function writeFactorySettingsForContext(ctx, patch) {
87
- if (patch.credentialSource !== undefined &&
88
- parseCredentialSource(patch.credentialSource) === undefined) {
89
- throw new Error(INVALID_CREDENTIAL_SOURCE_MESSAGE);
90
- }
91
- if (patch.maxConcurrent !== undefined &&
92
- parseSlotCount(patch.maxConcurrent) === undefined) {
93
- throw new Error(INVALID_SLOT_COUNT_MESSAGE);
94
- }
95
- const current = readFactorySettingsForContext(ctx);
96
- const next = { ...current };
97
- if (patch.maxConcurrent !== undefined) {
98
- next.maxConcurrent = patch.maxConcurrent;
99
- }
100
- if (patch.credentialSource !== undefined) {
101
- next.credentialSource = patch.credentialSource;
102
- }
103
- const out = {};
104
- if (next.maxConcurrent !== undefined) {
105
- out.maxConcurrent = next.maxConcurrent;
106
- }
107
- if (next.credentialSource !== undefined) {
108
- out.credentialSource = next.credentialSource;
109
- }
136
+ const next = applyFactorySettingsPatch(readFactorySettingsForContext(ctx), patch);
137
+ const out = serializeFactorySettings(next);
110
138
  ensureSettingsDirForContext(ctx);
111
139
  writeFileSync(settingsFilePathForContext(ctx), `${JSON.stringify(out, null, 2)}\n`, "utf8");
112
140
  }
113
141
  export function writeFactorySettings(cwd, patch) {
114
- if (patch.credentialSource !== undefined &&
115
- parseCredentialSource(patch.credentialSource) === undefined) {
116
- throw new Error(INVALID_CREDENTIAL_SOURCE_MESSAGE);
117
- }
118
- if (patch.maxConcurrent !== undefined &&
119
- parseSlotCount(patch.maxConcurrent) === undefined) {
120
- throw new Error(INVALID_SLOT_COUNT_MESSAGE);
121
- }
122
- const current = readFactorySettings(cwd);
123
- const next = { ...current };
124
- if (patch.maxConcurrent !== undefined) {
125
- next.maxConcurrent = patch.maxConcurrent;
126
- }
127
- if (patch.credentialSource !== undefined) {
128
- next.credentialSource = patch.credentialSource;
129
- }
130
- const out = {};
131
- if (next.maxConcurrent !== undefined) {
132
- out.maxConcurrent = next.maxConcurrent;
133
- }
134
- if (next.credentialSource !== undefined) {
135
- out.credentialSource = next.credentialSource;
136
- }
142
+ const next = applyFactorySettingsPatch(readFactorySettings(cwd), patch);
143
+ const out = serializeFactorySettings(next);
137
144
  const root = path.resolve(cwd);
138
145
  mkdirSync(storeRootFor(root), { recursive: true });
139
146
  writeFileSync(settingsFilePath(cwd), `${JSON.stringify(out, null, 2)}\n`, "utf8");
@@ -158,17 +165,17 @@ export function writeMaxConcurrentToGlobal(maxConcurrent) {
158
165
  throw new Error(INVALID_SLOT_COUNT_MESSAGE);
159
166
  }
160
167
  const current = parseFactorySettingsFromRaw(readRawSettingsFromFile(globalSettingsFilePath()));
161
- const next = { ...current, maxConcurrent };
162
- const out = {};
163
- if (next.maxConcurrent !== undefined) {
164
- out.maxConcurrent = next.maxConcurrent;
165
- }
166
- if (next.credentialSource !== undefined) {
167
- out.credentialSource = next.credentialSource;
168
- }
168
+ const next = applyFactorySettingsPatch(current, { maxConcurrent });
169
+ const out = serializeFactorySettings(next);
169
170
  mkdirSync(path.dirname(globalSettingsFilePath()), { recursive: true });
170
171
  writeFileSync(globalSettingsFilePath(), `${JSON.stringify(out, null, 2)}\n`, "utf8");
171
172
  }
173
+ export function readWorkshopModelFromFile(cwd) {
174
+ return readFactorySettings(cwd).workshopModel;
175
+ }
176
+ export function writeWorkshopModelToFile(cwd, workshopModel) {
177
+ writeFactorySettings(cwd, { workshopModel });
178
+ }
172
179
  export function readCredentialSourceFromFile(cwd) {
173
180
  return readFactorySettings(cwd).credentialSource;
174
181
  }