stageflow 0.27.0 → 0.31.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 (270) 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 +10 -0
  5. package/dist/agent/cursorProvider.js +83 -8
  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/providerAuth.d.ts +7 -3
  14. package/dist/agent/providerAuth.js +5 -3
  15. package/dist/agent/streamLogRedact.d.ts +6 -0
  16. package/dist/agent/streamLogRedact.js +23 -1
  17. package/dist/browser/anchor.d.ts +27 -0
  18. package/dist/browser/anchor.js +127 -0
  19. package/dist/browser/auditSink.d.ts +38 -0
  20. package/dist/browser/auditSink.js +34 -0
  21. package/dist/browser/browserHost.d.ts +86 -0
  22. package/dist/browser/browserHost.js +7 -0
  23. package/dist/browser/browserSweep.d.ts +14 -0
  24. package/dist/browser/browserSweep.js +54 -0
  25. package/dist/browser/browserTeardown.d.ts +56 -0
  26. package/dist/browser/browserTeardown.js +235 -0
  27. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
  28. package/dist/browser/fakeRemoteBrowserHost.js +34 -0
  29. package/dist/browser/gateHandoff.d.ts +25 -0
  30. package/dist/browser/gateHandoff.js +83 -0
  31. package/dist/browser/humanLogin.d.ts +18 -0
  32. package/dist/browser/humanLogin.js +34 -0
  33. package/dist/browser/keyProvider.d.ts +10 -0
  34. package/dist/browser/keyProvider.js +39 -0
  35. package/dist/browser/localBrowserHost.d.ts +10 -0
  36. package/dist/browser/localBrowserHost.js +152 -0
  37. package/dist/browser/localProfileLock.d.ts +2 -0
  38. package/dist/browser/localProfileLock.js +218 -0
  39. package/dist/browser/localProfileStore.d.ts +5 -0
  40. package/dist/browser/localProfileStore.js +91 -0
  41. package/dist/browser/loginCheck.d.ts +53 -0
  42. package/dist/browser/loginCheck.js +147 -0
  43. package/dist/browser/memoryProfileLock.d.ts +2 -0
  44. package/dist/browser/memoryProfileLock.js +53 -0
  45. package/dist/browser/memoryProfileStore.d.ts +2 -0
  46. package/dist/browser/memoryProfileStore.js +31 -0
  47. package/dist/browser/navigationAudit.d.ts +16 -0
  48. package/dist/browser/navigationAudit.js +53 -0
  49. package/dist/browser/persistedEnv.d.ts +3 -0
  50. package/dist/browser/persistedEnv.js +15 -0
  51. package/dist/browser/profileLock.d.ts +40 -0
  52. package/dist/browser/profileLock.js +10 -0
  53. package/dist/browser/profileStore.d.ts +23 -0
  54. package/dist/browser/profileStore.js +39 -0
  55. package/dist/browser/runLiveness.d.ts +13 -0
  56. package/dist/browser/runLiveness.js +39 -0
  57. package/dist/browser/sitePolicy.d.ts +10 -0
  58. package/dist/browser/sitePolicy.js +61 -0
  59. package/dist/browser/stageBrowserEnv.d.ts +22 -0
  60. package/dist/browser/stageBrowserEnv.js +147 -0
  61. package/dist/browser/stageProfileLock.d.ts +17 -0
  62. package/dist/browser/stageProfileLock.js +39 -0
  63. package/dist/cli/browserCommand.d.ts +44 -0
  64. package/dist/cli/browserCommand.js +472 -0
  65. package/dist/cli/doctorCommand.js +7 -0
  66. package/dist/cli/hostClient.d.ts +20 -0
  67. package/dist/cli/hostClient.js +36 -2
  68. package/dist/cli/initCommand.d.ts +1 -0
  69. package/dist/cli/initCommand.js +23 -7
  70. package/dist/cli/initTemplates.d.ts +5 -2
  71. package/dist/cli/initTemplates.js +25 -2
  72. package/dist/cli/providersCommand.d.ts +1 -1
  73. package/dist/cli/providersCommand.js +4 -4
  74. package/dist/cli/runsCommand.js +6 -2
  75. package/dist/cli/triggerCommand.d.ts +12 -0
  76. package/dist/cli/triggerCommand.js +183 -0
  77. package/dist/cli/validateCommand.d.ts +1 -1
  78. package/dist/cli/validateCommand.js +23 -6
  79. package/dist/cli.js +32 -3
  80. package/dist/config/browseCatalog.d.ts +1 -0
  81. package/dist/config/browseCatalog.js +5 -4
  82. package/dist/config/builtinSkills.d.ts +4 -0
  83. package/dist/config/builtinSkills.js +23 -0
  84. package/dist/config/catalogRelativePath.d.ts +1 -0
  85. package/dist/config/catalogRelativePath.js +10 -2
  86. package/dist/config/createTrigger.d.ts +27 -0
  87. package/dist/config/createTrigger.js +232 -0
  88. package/dist/config/draftPackage.d.ts +117 -0
  89. package/dist/config/draftPackage.js +550 -0
  90. package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
  91. package/dist/config/ensureCatalogScanRoot.js +73 -0
  92. package/dist/config/hostConfig.d.ts +2 -0
  93. package/dist/config/hostConfig.js +27 -0
  94. package/dist/config/listSkills.d.ts +2 -2
  95. package/dist/config/listSkills.js +17 -3
  96. package/dist/config/loadOutcome.d.ts +6 -0
  97. package/dist/config/loadPipeline.js +16 -0
  98. package/dist/config/loadStage.js +7 -0
  99. package/dist/config/loadStageflowManifest.js +25 -0
  100. package/dist/config/loadTrigger.d.ts +9 -0
  101. package/dist/config/loadTrigger.js +120 -0
  102. package/dist/config/multiProjectCatalog.d.ts +2 -0
  103. package/dist/config/multiProjectCatalog.js +26 -18
  104. package/dist/config/normalizePipelineStageEntry.js +8 -1
  105. package/dist/config/parseCompletionContract.js +6 -0
  106. package/dist/config/pipelineStageKeys.js +1 -0
  107. package/dist/config/publishDraftPackage.d.ts +7 -0
  108. package/dist/config/publishDraftPackage.js +25 -0
  109. package/dist/config/resolveCatalogContext.js +1 -1
  110. package/dist/config/resolvePipelineDag.js +1 -1
  111. package/dist/config/scanCatalogPaths.d.ts +1 -1
  112. package/dist/config/scanCatalogPaths.js +6 -2
  113. package/dist/config/stageBrowser.d.ts +12 -0
  114. package/dist/config/stageBrowser.js +118 -0
  115. package/dist/config/triggerCatalog.d.ts +10 -0
  116. package/dist/config/triggerCatalog.js +37 -0
  117. package/dist/config/validateCatalog.d.ts +13 -3
  118. package/dist/config/validateCatalog.js +83 -2
  119. package/dist/config/validateCompletionContract.js +8 -0
  120. package/dist/envelope/preEmitChecks.d.ts +1 -1
  121. package/dist/envelope/preEmitChecks.js +8 -0
  122. package/dist/logging/redact.d.ts +4 -0
  123. package/dist/logging/redact.js +17 -0
  124. package/dist/mcp/catalogTools.js +43 -8
  125. package/dist/mcp/controlTools.js +3 -1
  126. package/dist/mcp/deps.d.ts +2 -0
  127. package/dist/mcp/server.js +1 -0
  128. package/dist/mcp/tools.js +2 -0
  129. package/dist/mcp/triggerTools.d.ts +3 -0
  130. package/dist/mcp/triggerTools.js +176 -0
  131. package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
  132. package/dist/operatorAgent/docsRetrieval.js +269 -0
  133. package/dist/operatorAgent/draftContext.d.ts +16 -0
  134. package/dist/operatorAgent/draftContext.js +68 -0
  135. package/dist/operatorAgent/fakeHost.d.ts +39 -0
  136. package/dist/operatorAgent/fakeHost.js +92 -0
  137. package/dist/operatorAgent/host.d.ts +40 -0
  138. package/dist/operatorAgent/host.js +179 -0
  139. package/dist/operatorAgent/index.d.ts +9 -0
  140. package/dist/operatorAgent/index.js +8 -0
  141. package/dist/operatorAgent/piModel.d.ts +80 -0
  142. package/dist/operatorAgent/piModel.js +528 -0
  143. package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
  144. package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
  145. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
  146. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
  147. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
  148. package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
  149. package/dist/operatorAgent/proposals.d.ts +32 -0
  150. package/dist/operatorAgent/proposals.js +144 -0
  151. package/dist/operatorAgent/types.d.ts +128 -0
  152. package/dist/operatorAgent/types.js +1 -0
  153. package/dist/package-meta.d.ts +1 -1
  154. package/dist/package-meta.js +1 -1
  155. package/dist/project/findManifestRoot.d.ts +3 -0
  156. package/dist/project/findManifestRoot.js +35 -0
  157. package/dist/project/resolveStageflowContext.d.ts +7 -0
  158. package/dist/project/resolveStageflowContext.js +44 -28
  159. package/dist/runstore/assertTmpdir.js +6 -2
  160. package/dist/runstore/diskUsage.d.ts +1 -1
  161. package/dist/runstore/diskUsage.js +2 -7
  162. package/dist/runstore/port.d.ts +28 -0
  163. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
  164. package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
  165. package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
  166. package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
  167. package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
  168. package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
  169. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
  170. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
  171. package/dist/runstore/sqlite/migrations/index.js +6 -0
  172. package/dist/runtime/answerResume.d.ts +1 -0
  173. package/dist/runtime/answerResume.js +3 -1
  174. package/dist/runtime/completionCheckRunner.d.ts +12 -1
  175. package/dist/runtime/completionCheckRunner.js +22 -0
  176. package/dist/runtime/credentialBinding.d.ts +7 -2
  177. package/dist/runtime/credentialBinding.js +20 -28
  178. package/dist/runtime/emailSource.d.ts +72 -0
  179. package/dist/runtime/emailSource.js +304 -0
  180. package/dist/runtime/githubPollSource.d.ts +86 -0
  181. package/dist/runtime/githubPollSource.js +284 -0
  182. package/dist/runtime/pipelineRunner.d.ts +4 -0
  183. package/dist/runtime/pipelineRunner.js +3 -0
  184. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  185. package/dist/runtime/pipelineScheduler.js +86 -1
  186. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  187. package/dist/runtime/runManager.d.ts +11 -0
  188. package/dist/runtime/runManager.js +161 -3
  189. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  190. package/dist/runtime/runRetryCoordinator.js +1 -0
  191. package/dist/runtime/scheduleSource.d.ts +38 -0
  192. package/dist/runtime/scheduleSource.js +119 -0
  193. package/dist/runtime/settingsFile.d.ts +7 -2
  194. package/dist/runtime/settingsFile.js +66 -56
  195. package/dist/runtime/stageAttemptBootstrap.js +55 -3
  196. package/dist/runtime/stageEnvironment.js +1 -0
  197. package/dist/runtime/stageHitl.d.ts +1 -0
  198. package/dist/runtime/stageHitl.js +5 -1
  199. package/dist/runtime/stageProcessLauncher.d.ts +3 -0
  200. package/dist/runtime/stageProcessLauncher.js +52 -5
  201. package/dist/runtime/stageRunner.d.ts +5 -0
  202. package/dist/runtime/stageRunner.js +30 -6
  203. package/dist/runtime/stageStreamLog.d.ts +1 -0
  204. package/dist/runtime/stageStreamLog.js +2 -1
  205. package/dist/runtime/triggerCatalog.d.ts +6 -0
  206. package/dist/runtime/triggerCatalog.js +24 -0
  207. package/dist/runtime/triggerEventMatch.d.ts +2 -0
  208. package/dist/runtime/triggerEventMatch.js +6 -0
  209. package/dist/runtime/triggerPort.d.ts +14 -0
  210. package/dist/runtime/triggerPort.js +1 -0
  211. package/dist/runtime/triggerRunner.d.ts +17 -0
  212. package/dist/runtime/triggerRunner.js +89 -0
  213. package/dist/runtime/verifiedStageExecution.d.ts +7 -0
  214. package/dist/runtime/verifiedStageExecution.js +14 -0
  215. package/dist/runtime/webhookSignature.d.ts +1 -0
  216. package/dist/runtime/webhookSignature.js +12 -0
  217. package/dist/server/bootstrap.d.ts +6 -0
  218. package/dist/server/bootstrap.js +70 -0
  219. package/dist/server/controlToken.js +13 -0
  220. package/dist/server/createHttpHost.js +3 -0
  221. package/dist/server/healthSurfaces.js +31 -6
  222. package/dist/server/http.d.ts +15 -0
  223. package/dist/server/http.js +1251 -74
  224. package/dist/server/requestAuthContext.d.ts +1 -1
  225. package/dist/server/triggerRoutes.d.ts +15 -0
  226. package/dist/server/triggerRoutes.js +268 -0
  227. package/dist/tools/askOperator.d.ts +3 -2
  228. package/dist/types/completion.d.ts +3 -0
  229. package/dist/types/pipeline.d.ts +3 -1
  230. package/dist/types/preEmitCheck.d.ts +7 -0
  231. package/dist/types/stage.d.ts +14 -0
  232. package/dist/types/stageflowManifest.d.ts +3 -0
  233. package/dist/types/trigger.d.ts +18 -0
  234. package/dist/types/trigger.js +1 -0
  235. package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
  236. package/dist/ui/assets/index-p8Iou3zk.js +133 -0
  237. package/dist/ui/index.html +2 -2
  238. package/dist/workshop/autosave.d.ts +58 -0
  239. package/dist/workshop/autosave.js +222 -0
  240. package/dist/workshop/buildStore.d.ts +79 -0
  241. package/dist/workshop/buildStore.js +326 -0
  242. package/dist/workshop/chatTurn.d.ts +232 -0
  243. package/dist/workshop/chatTurn.js +475 -0
  244. package/dist/workshop/modelSettings.d.ts +18 -0
  245. package/dist/workshop/modelSettings.js +44 -0
  246. package/dist/workshop/sessionStore.d.ts +61 -0
  247. package/dist/workshop/sessionStore.js +254 -0
  248. package/dist/workshop/workshopPin.d.ts +72 -0
  249. package/dist/workshop/workshopPin.js +193 -0
  250. package/examples/README.md +2 -1
  251. package/examples/browser-session/README.md +77 -0
  252. package/examples/browser-session/browser-session.pipeline.yaml +50 -0
  253. package/examples/browser-session/browser-session.task.yaml +6 -0
  254. package/examples/browser-session/check-login.yaml +22 -0
  255. package/examples/browser-session/fixture-server.mjs +64 -0
  256. package/examples/browser-session/human-login.yaml +31 -0
  257. package/examples/browser-session/merge.yaml +36 -0
  258. package/examples/browser-session/work-a.yaml +41 -0
  259. package/examples/browser-session/work-b.yaml +41 -0
  260. package/examples/conditional-fork/decide.yaml +1 -1
  261. package/examples/github-release/README.md +17 -3
  262. package/examples/stage-mcp/README.md +3 -2
  263. package/examples/stageflow.yaml +51 -0
  264. package/package.json +9 -4
  265. package/skills/stageflow/references/control-surface.md +3 -1
  266. package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
  267. package/skills/stageflow-run/SKILL.md +2 -0
  268. package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
  269. package/skills/stageflow-setup/SKILL.md +11 -0
  270. 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,16 +1946,40 @@ 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,
1907
1979
  sessionFilePath,
1908
1980
  env: stageBinding.env,
1909
1981
  bindingKind: stageBinding.kind,
1982
+ ...(resumedStage !== undefined ? { model: resumedStage.model } : {}),
1910
1983
  ...(this.options.operatorCatalog !== undefined
1911
1984
  ? { operatorCatalog: this.options.operatorCatalog }
1912
1985
  : {}),
@@ -1914,12 +1987,53 @@ export class RunManager {
1914
1987
  if (launchResult.type === "waiting") {
1915
1988
  return { ok: true };
1916
1989
  }
1990
+ await teardownStageBrowser(this.options.browser ?? defaultStageBrowserSupport(), {
1991
+ runId,
1992
+ runDir: workspaceDir,
1993
+ stageId,
1994
+ events: () => store.listStageEvents(runId, stageId),
1995
+ }).catch(() => undefined);
1917
1996
  if (launchResult.type === "failed") {
1997
+ const nextRepairAttempt = await this.scheduleRepairAfterVerifyFailure(runId, stageId, meta.pipeline_dag);
1998
+ if (nextRepairAttempt !== undefined) {
1999
+ await store.updateRunStatus(runId, "running");
2000
+ const repaired = await retryRun({
2001
+ prepared: {
2002
+ task,
2003
+ loaded,
2004
+ run: { runId, workspaceDir: store.getWorkspaceDir(runId) },
2005
+ agent: this.options.agent,
2006
+ store,
2007
+ cwd: meta.project_root ?? this.projectRoot,
2008
+ projectRoot: meta.project_root ?? this.projectRoot,
2009
+ checkoutRoot: meta.checkout_root,
2010
+ hitl: this.hitl,
2011
+ operatorCatalog: this.options.operatorCatalog,
2012
+ ...(this.options.browser !== undefined
2013
+ ? { browser: this.options.browser }
2014
+ : {}),
2015
+ },
2016
+ retryRoots: new Map([[stageId, nextRepairAttempt]]),
2017
+ maxActiveStagesPerRun: this.maxActiveStagesPerRun,
2018
+ executionMode: this.executionMode,
2019
+ stageProcessLauncher: launcher,
2020
+ schedulingHalt: this.ensureSchedulingHalt(runId),
2021
+ });
2022
+ if (repaired.outcome === "failed") {
2023
+ return { ok: false, reason: repaired.reason };
2024
+ }
2025
+ return { ok: true, verification: "failed_retrying" };
2026
+ }
1918
2027
  await store.appendStageEvent(runId, stageId, {
1919
2028
  event: "failed",
1920
2029
  reason: launchResult.reason,
1921
2030
  }, eventOptions);
1922
2031
  await store.updateRunStatus(runId, "failed");
2032
+ await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
2033
+ runId,
2034
+ runDir: workspaceDir,
2035
+ events: (stageId) => store.listStageEvents(runId, stageId),
2036
+ }).catch(() => undefined);
1923
2037
  return { ok: false, reason: launchResult.reason };
1924
2038
  }
1925
2039
  let initialPrior = null;
@@ -1941,6 +2055,9 @@ export class RunManager {
1941
2055
  checkoutRoot: meta.checkout_root,
1942
2056
  hitl: this.hitl,
1943
2057
  operatorCatalog: this.options.operatorCatalog,
2058
+ ...(this.options.browser !== undefined
2059
+ ? { browser: this.options.browser }
2060
+ : {}),
1944
2061
  },
1945
2062
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
1946
2063
  resumeFromStageId: stageId,
@@ -1964,6 +2081,11 @@ export class RunManager {
1964
2081
  reason,
1965
2082
  }, eventOptions);
1966
2083
  await store.updateRunStatus(runId, "failed");
2084
+ await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
2085
+ runId,
2086
+ runDir: store.getWorkspaceDir(runId),
2087
+ events: (stageId) => store.listStageEvents(runId, stageId),
2088
+ }).catch(() => undefined);
1967
2089
  }
1968
2090
  catch {
1969
2091
  // ignore secondary failures
@@ -1974,6 +2096,24 @@ export class RunManager {
1974
2096
  this.attachedWaiting.delete(`${runId}\0${stageId}`);
1975
2097
  }
1976
2098
  }
2099
+ /**
2100
+ * A resumed stage whose after-phase verification failed is repaired by the
2101
+ * same policy the scheduler applies to a first-launch failure. Returns the
2102
+ * freshly created attempt, or undefined when the failure is final.
2103
+ */
2104
+ async scheduleRepairAfterVerifyFailure(runId, stageId, dag) {
2105
+ const store = this.options.store;
2106
+ const recovery = dag?.nodes.find((node) => node.id === stageId)?.recovery;
2107
+ if (recovery?.mode !== "repair")
2108
+ return undefined;
2109
+ const latest = await store.getLatestStageExecution(runId, stageId);
2110
+ if (latest?.verification_outcome !== "failed")
2111
+ return undefined;
2112
+ const attempts = await store.countStageAttempts(runId, stageId);
2113
+ if (attempts >= recovery.max_attempts)
2114
+ return undefined;
2115
+ return (await store.createStageExecution(runId, stageId)).attempt;
2116
+ }
1977
2117
  async reconstructAndContinue(runId, stageId, opaqueAnswer) {
1978
2118
  try {
1979
2119
  return await resumeReconstructAndContinue({
@@ -2248,6 +2388,9 @@ export class RunManager {
2248
2388
  const schedulingHalt = this.ensureSchedulingHalt(materialized.runId);
2249
2389
  const started = await startPipeline({
2250
2390
  submission,
2391
+ ...(this.options.browser !== undefined
2392
+ ? { browser: this.options.browser }
2393
+ : {}),
2251
2394
  agent: this.options.agent,
2252
2395
  store: this.options.store,
2253
2396
  taskYaml,
@@ -2364,17 +2507,26 @@ export class RunManager {
2364
2507
  };
2365
2508
  }
2366
2509
  async checkDiskFloorAdmission() {
2510
+ const raw = process.env.STAGEFLOW_MIN_FREE_DISK_BYTES;
2511
+ if (raw === undefined || raw.trim() === "") {
2512
+ return undefined;
2513
+ }
2367
2514
  try {
2368
2515
  const readFree = this.options.freeSpaceReader ?? readFilesystemSize;
2369
2516
  const size = await readFree(globalStageflowHome());
2370
- const minFreeBytes = resolveMinFreeDiskFloor(process.env.STAGEFLOW_MIN_FREE_DISK_BYTES, size.totalBytes);
2517
+ const minFreeBytes = resolveMinFreeDiskFloor(raw, size.totalBytes);
2518
+ if (minFreeBytes === 0) {
2519
+ return undefined;
2520
+ }
2371
2521
  if (size.freeBytes < minFreeBytes) {
2372
2522
  return this.insufficientDiskFailure(size.freeBytes, minFreeBytes);
2373
2523
  }
2374
2524
  return undefined;
2375
2525
  }
2376
2526
  catch (err) {
2377
- // Default floor is always active via resolveMinFreeDiskFloor — fail closed.
2527
+ if (resolveMinFreeDiskFloor(raw, 0) === 0 && !raw.trim().endsWith("%")) {
2528
+ return undefined;
2529
+ }
2378
2530
  return {
2379
2531
  ok: false,
2380
2532
  reason: `Disk free-space check failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -2738,6 +2890,9 @@ export class RunManager {
2738
2890
  const schedulingHalt = this.ensureSchedulingHalt(runId);
2739
2891
  const started = await startPipeline({
2740
2892
  submission,
2893
+ ...(this.options.browser !== undefined
2894
+ ? { browser: this.options.browser }
2895
+ : {}),
2741
2896
  agent: this.options.agent,
2742
2897
  store: this.options.store,
2743
2898
  taskYaml,
@@ -2842,6 +2997,9 @@ export class RunManager {
2842
2997
  checkoutRoot: loadedMeta.checkout_root,
2843
2998
  hitl: this.hitl,
2844
2999
  operatorCatalog: this.options.operatorCatalog,
3000
+ ...(this.options.browser !== undefined
3001
+ ? { browser: this.options.browser }
3002
+ : {}),
2845
3003
  },
2846
3004
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
2847
3005
  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
+ }
@@ -2,17 +2,20 @@ import type { ProjectContext } from "../project/resolveProjectContext.js";
2
2
  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
- export declare const INVALID_CREDENTIAL_SOURCE_MESSAGE = "credentialSource must be \"pi_home\" or \"sf_owned\"";
6
- export type CredentialSource = "pi_home" | "sf_owned";
5
+ export declare const INVALID_CREDENTIAL_SOURCE_MESSAGE = "credentialSource must be \"sf_owned\"";
6
+ export declare const INVALID_WORKSHOP_MODEL_MESSAGE = "workshopModel must be a non-empty string";
7
+ export type CredentialSource = "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;