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
@@ -0,0 +1,86 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { TriggerFireEvent, TriggerSourcePort } from "./triggerPort.js";
3
+ export declare const DEFAULT_GITHUB_POLL_INTERVAL_MS = 60000;
4
+ export declare const DEFAULT_GITHUB_TOKEN_SECRET = "GITHUB_TOKEN";
5
+ /** Parse `STAGEFLOW_GITHUB_POLL_INTERVAL_MS`; default 60s; `0` disables. */
6
+ export declare function githubPollIntervalMsFromEnv(env?: NodeJS.ProcessEnv): number;
7
+ export type GithubPullItem = {
8
+ number: number;
9
+ title: string;
10
+ html_url: string;
11
+ state: string;
12
+ merged_at: string | null;
13
+ created_at: string;
14
+ updated_at: string;
15
+ user: {
16
+ login: string;
17
+ } | null;
18
+ };
19
+ export type GithubPollResponse = {
20
+ status: number;
21
+ etag?: string;
22
+ items: GithubPullItem[];
23
+ };
24
+ /**
25
+ * Narrow, test-friendly seam over "list PR activity for a repo, conditionally
26
+ * on an ETag". The default implementation adapts `octokit.rest.pulls.list`;
27
+ * tests inject a fake so no real GitHub calls happen.
28
+ */
29
+ export type GithubPollClient = {
30
+ listPulls(params: {
31
+ owner: string;
32
+ repo: string;
33
+ etag?: string;
34
+ }): Promise<GithubPollResponse>;
35
+ };
36
+ export declare function defaultCreateGithubClient(token: string): GithubPollClient;
37
+ export type NormalizedGithubEvent = {
38
+ action: "opened" | "merged" | "closed" | "updated";
39
+ number: number;
40
+ title: string;
41
+ url: string;
42
+ author: string;
43
+ repo: string;
44
+ updatedAt: string;
45
+ };
46
+ export type GithubPollSourceOptions = {
47
+ store: RunStore;
48
+ cwd?: string;
49
+ intervalMs?: number;
50
+ logError?: (message: string) => void;
51
+ env?: NodeJS.ProcessEnv;
52
+ /** Injectable client factory so tests never hit real GitHub. */
53
+ createGithubClient?: (token: string) => GithubPollClient;
54
+ };
55
+ /**
56
+ * `TriggerSourcePort` adapter for `event`-kind triggers whose `event.source`
57
+ * starts with `github.`. Ticks on an interval; each tick re-scans the catalog,
58
+ * groups triggers by their distinct `event.config.repo` (one API call per repo
59
+ * per tick regardless of how many triggers watch it), and diffs each repo's
60
+ * PR list against a persisted ETag + cursor to find genuinely new activity.
61
+ *
62
+ * The first tick for a never-before-seen repo only seeds the cursor — it
63
+ * never fires for a repo's pre-existing PR history, mirroring how
64
+ * `ScheduleSource` seeds `next_run_at` without firing on first sight.
65
+ */
66
+ export declare class GithubPollSource implements TriggerSourcePort {
67
+ private readonly store;
68
+ private readonly cwd;
69
+ private readonly intervalMs;
70
+ private readonly logError;
71
+ private readonly env;
72
+ private readonly createGithubClient;
73
+ private interval;
74
+ private inFlight;
75
+ constructor(options: GithubPollSourceOptions);
76
+ start(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
77
+ stop(): Promise<void>;
78
+ /**
79
+ * One discovery + poll + fire pass. Public so tests can drive it directly
80
+ * instead of waiting on real interval timers.
81
+ */
82
+ tick(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
83
+ private discover;
84
+ private pollRepo;
85
+ private fireSafely;
86
+ }
@@ -0,0 +1,284 @@
1
+ import path from "node:path";
2
+ import { Octokit } from "octokit";
3
+ import { getCatalogScanPaths } from "../config/browseCatalog.js";
4
+ import { loadTriggerOutcome } from "../config/loadTrigger.js";
5
+ import { catalogContextFromStageflow } from "../config/resolveCatalogContext.js";
6
+ import { readSecretFromEnvOrFile } from "../config/secretFromEnvOrFile.js";
7
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
8
+ import { matchesEventFilter } from "./triggerEventMatch.js";
9
+ export const DEFAULT_GITHUB_POLL_INTERVAL_MS = 60_000;
10
+ export const DEFAULT_GITHUB_TOKEN_SECRET = "GITHUB_TOKEN";
11
+ const GITHUB_REPO_RE = /^[^\s/]+\/[^\s/]+$/;
12
+ /** Parse `STAGEFLOW_GITHUB_POLL_INTERVAL_MS`; default 60s; `0` disables. */
13
+ export function githubPollIntervalMsFromEnv(env = process.env) {
14
+ const raw = env.STAGEFLOW_GITHUB_POLL_INTERVAL_MS;
15
+ if (raw === undefined || raw.trim() === "")
16
+ return DEFAULT_GITHUB_POLL_INTERVAL_MS;
17
+ const parsed = Number(raw);
18
+ if (!Number.isFinite(parsed) || parsed < 0)
19
+ return DEFAULT_GITHUB_POLL_INTERVAL_MS;
20
+ return parsed;
21
+ }
22
+ export function defaultCreateGithubClient(token) {
23
+ const octokit = new Octokit({ auth: token });
24
+ return {
25
+ async listPulls({ owner, repo, etag }) {
26
+ let response;
27
+ try {
28
+ response = await octokit.rest.pulls.list({
29
+ owner,
30
+ repo,
31
+ state: "all",
32
+ sort: "updated",
33
+ direction: "desc",
34
+ per_page: 30,
35
+ ...(etag !== undefined ? { headers: { "If-None-Match": etag } } : {}),
36
+ });
37
+ }
38
+ catch (err) {
39
+ if (err !== null && typeof err === "object" && "status" in err && err.status === 304) {
40
+ return { status: 304, items: [] };
41
+ }
42
+ throw err;
43
+ }
44
+ const responseEtag = response.headers.etag;
45
+ return {
46
+ status: response.status,
47
+ ...(typeof responseEtag === "string" ? { etag: responseEtag } : {}),
48
+ items: response.data.map((pr) => ({
49
+ number: pr.number,
50
+ title: pr.title,
51
+ html_url: pr.html_url,
52
+ state: pr.state,
53
+ merged_at: pr.merged_at,
54
+ created_at: pr.created_at,
55
+ updated_at: pr.updated_at,
56
+ user: pr.user ? { login: pr.user.login } : null,
57
+ })),
58
+ };
59
+ },
60
+ };
61
+ }
62
+ function deriveAction(item) {
63
+ if (item.merged_at)
64
+ return "merged";
65
+ if (item.state === "closed")
66
+ return "closed";
67
+ if (item.created_at === item.updated_at)
68
+ return "opened";
69
+ return "updated";
70
+ }
71
+ function normalizeGithubItem(item, repo) {
72
+ return {
73
+ action: deriveAction(item),
74
+ number: item.number,
75
+ title: item.title,
76
+ url: item.html_url,
77
+ author: item.user?.login ?? "unknown",
78
+ repo,
79
+ updatedAt: item.updated_at,
80
+ };
81
+ }
82
+ function parseCursor(raw) {
83
+ if (raw === null)
84
+ return null;
85
+ try {
86
+ const parsed = JSON.parse(raw);
87
+ if (typeof parsed.updatedAt === "string" && typeof parsed.number === "number") {
88
+ return { updatedAt: parsed.updatedAt, number: parsed.number };
89
+ }
90
+ }
91
+ catch {
92
+ // Corrupted/foreign value; treat as unset.
93
+ }
94
+ return null;
95
+ }
96
+ function isNewerThanCursor(item, cursor) {
97
+ const itemMs = new Date(item.updated_at).getTime();
98
+ const cursorMs = new Date(cursor.updatedAt).getTime();
99
+ if (itemMs !== cursorMs)
100
+ return itemMs > cursorMs;
101
+ return item.number > cursor.number;
102
+ }
103
+ function firstSecretRef(matches) {
104
+ for (const { definition } of matches) {
105
+ const secretRef = definition.event?.config?.secretRef;
106
+ if (typeof secretRef === "string" && secretRef.length > 0)
107
+ return secretRef;
108
+ }
109
+ return undefined;
110
+ }
111
+ /**
112
+ * `TriggerSourcePort` adapter for `event`-kind triggers whose `event.source`
113
+ * starts with `github.`. Ticks on an interval; each tick re-scans the catalog,
114
+ * groups triggers by their distinct `event.config.repo` (one API call per repo
115
+ * per tick regardless of how many triggers watch it), and diffs each repo's
116
+ * PR list against a persisted ETag + cursor to find genuinely new activity.
117
+ *
118
+ * The first tick for a never-before-seen repo only seeds the cursor — it
119
+ * never fires for a repo's pre-existing PR history, mirroring how
120
+ * `ScheduleSource` seeds `next_run_at` without firing on first sight.
121
+ */
122
+ export class GithubPollSource {
123
+ store;
124
+ cwd;
125
+ intervalMs;
126
+ logError;
127
+ env;
128
+ createGithubClient;
129
+ interval;
130
+ inFlight = false;
131
+ constructor(options) {
132
+ this.store = options.store;
133
+ this.cwd = options.cwd ?? process.cwd();
134
+ this.intervalMs = options.intervalMs ?? DEFAULT_GITHUB_POLL_INTERVAL_MS;
135
+ this.logError =
136
+ options.logError ??
137
+ ((message) => {
138
+ console.error(message);
139
+ });
140
+ this.env = options.env ?? process.env;
141
+ this.createGithubClient = options.createGithubClient ?? defaultCreateGithubClient;
142
+ }
143
+ async start(onFire) {
144
+ if (this.intervalMs <= 0)
145
+ return;
146
+ this.interval = setInterval(() => {
147
+ if (this.inFlight)
148
+ return;
149
+ this.inFlight = true;
150
+ void this.tick(onFire)
151
+ .catch((err) => {
152
+ this.logError(`github poll tick failed: ${err instanceof Error ? err.message : String(err)}`);
153
+ })
154
+ .finally(() => {
155
+ this.inFlight = false;
156
+ });
157
+ }, this.intervalMs).unref();
158
+ }
159
+ async stop() {
160
+ if (this.interval !== undefined)
161
+ clearInterval(this.interval);
162
+ this.interval = undefined;
163
+ }
164
+ /**
165
+ * One discovery + poll + fire pass. Public so tests can drive it directly
166
+ * instead of waiting on real interval timers.
167
+ */
168
+ async tick(onFire) {
169
+ const ctx = catalogContextFromStageflow(await resolveStageflowContext(this.cwd));
170
+ const scanPaths = await getCatalogScanPaths(ctx);
171
+ if (!scanPaths)
172
+ return;
173
+ const projectRoot = ctx.projectRoot ?? undefined;
174
+ const groups = await this.discover(scanPaths.triggerPaths);
175
+ for (const [repo, matches] of groups) {
176
+ for (const match of matches) {
177
+ const definitionRef = projectRoot !== undefined
178
+ ? path.relative(projectRoot, match.path).replace(/\\/g, "/")
179
+ : match.path;
180
+ await this.store.upsertTrigger({
181
+ id: match.definition.id,
182
+ definitionRef,
183
+ enabled: true,
184
+ });
185
+ }
186
+ await this.pollRepo(repo, matches, onFire);
187
+ }
188
+ }
189
+ async discover(triggerPaths) {
190
+ const groups = new Map();
191
+ for (const triggerPath of triggerPaths) {
192
+ const outcome = await loadTriggerOutcome(triggerPath);
193
+ if (!outcome.ok)
194
+ continue;
195
+ const definition = outcome.value;
196
+ if (definition.kind !== "event" || !definition.enabled)
197
+ continue;
198
+ const source = definition.event?.source;
199
+ if (typeof source !== "string" || !source.startsWith("github."))
200
+ continue;
201
+ const repo = definition.event?.config?.repo;
202
+ if (typeof repo !== "string" || !GITHUB_REPO_RE.test(repo)) {
203
+ this.logError(`github poll: trigger "${definition.id}" has an invalid event.config.repo ` +
204
+ `(expected "owner/repo"); skipping`);
205
+ continue;
206
+ }
207
+ const list = groups.get(repo) ?? [];
208
+ list.push({ path: triggerPath, definition });
209
+ groups.set(repo, list);
210
+ }
211
+ return groups;
212
+ }
213
+ async pollRepo(repo, matches, onFire) {
214
+ const secretRef = firstSecretRef(matches) ?? DEFAULT_GITHUB_TOKEN_SECRET;
215
+ let token;
216
+ try {
217
+ token = readSecretFromEnvOrFile(this.env, secretRef);
218
+ }
219
+ catch (err) {
220
+ this.logError(`github poll: failed to read secret "${secretRef}" for repo "${repo}": ${err instanceof Error ? err.message : String(err)}`);
221
+ return;
222
+ }
223
+ if (token === undefined) {
224
+ this.logError(`github poll: secret "${secretRef}" is not set for repo "${repo}"; skipping this tick`);
225
+ return;
226
+ }
227
+ const adapterId = `github:${repo}`;
228
+ const etag = await this.store.getTriggerAdapterState(adapterId, "etag");
229
+ const cursor = parseCursor(await this.store.getTriggerAdapterState(adapterId, "cursor"));
230
+ const [owner, repoName] = repo.split("/");
231
+ let response;
232
+ try {
233
+ const client = this.createGithubClient(token);
234
+ response = await client.listPulls({
235
+ owner,
236
+ repo: repoName,
237
+ ...(etag !== null ? { etag } : {}),
238
+ });
239
+ }
240
+ catch (err) {
241
+ this.logError(`github poll: request failed for repo "${repo}": ${err instanceof Error ? err.message : String(err)}`);
242
+ return;
243
+ }
244
+ if (response.status === 304)
245
+ return;
246
+ if (response.etag !== undefined) {
247
+ await this.store.setTriggerAdapterState(adapterId, "etag", response.etag);
248
+ }
249
+ if (response.items.length === 0)
250
+ return;
251
+ const newest = response.items[0];
252
+ if (cursor === null) {
253
+ await this.store.setTriggerAdapterState(adapterId, "cursor", JSON.stringify({ updatedAt: newest.updated_at, number: newest.number }));
254
+ return;
255
+ }
256
+ const newItems = response.items.filter((item) => isNewerThanCursor(item, cursor)).reverse();
257
+ for (const item of newItems) {
258
+ const summary = normalizeGithubItem(item, repo);
259
+ for (const { definition } of matches) {
260
+ if (!matchesEventFilter(definition.event?.match, summary))
261
+ continue;
262
+ await this.fireSafely(onFire, definition, summary);
263
+ }
264
+ }
265
+ await this.store.setTriggerAdapterState(adapterId, "cursor", JSON.stringify({ updatedAt: newest.updated_at, number: newest.number }));
266
+ }
267
+ async fireSafely(onFire, definition, summary) {
268
+ try {
269
+ if (definition.task !== undefined) {
270
+ await onFire({ triggerId: definition.id });
271
+ return;
272
+ }
273
+ const task = {
274
+ id: `${definition.id}-pr-${summary.number}`,
275
+ goal: `Handle PR #${summary.number}: ${summary.title}`,
276
+ input: summary,
277
+ };
278
+ await onFire({ triggerId: definition.id, task });
279
+ }
280
+ catch (err) {
281
+ this.logError(`github poll: firing trigger "${definition.id}" failed: ${err instanceof Error ? err.message : String(err)}`);
282
+ }
283
+ }
284
+ }
@@ -7,6 +7,7 @@ import type { StageHitlController } from "./stageHitl.js";
7
7
  import type { InlinePipelineDefinition, LoadedPipeline } from "../types/pipeline.js";
8
8
  import type { TaskFile } from "../types/task.js";
9
9
  import { type StageExecutionMode } from "./stageConcurrency.js";
10
+ import type { StageBrowserSupport } from "../browser/browserHost.js";
10
11
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
11
12
  import type { OperatorCatalog } from "./stageAttemptBootstrap.js";
12
13
  import { type SkillsPayload } from "./runSkills.js";
@@ -59,6 +60,7 @@ export type PreparedPipeline = {
59
60
  hitl?: StageHitlController;
60
61
  executionMode?: StageExecutionMode;
61
62
  stageProcessLauncher?: StageProcessLauncher;
63
+ browser?: StageBrowserSupport;
62
64
  operatorCatalog?: OperatorCatalog;
63
65
  skipGates?: boolean;
64
66
  findings?: ValidationFinding[];
@@ -89,6 +91,7 @@ export declare function runPipeline(options: {
89
91
  maxActiveStagesPerRun?: number;
90
92
  executionMode?: StageExecutionMode;
91
93
  stageProcessLauncher?: StageProcessLauncher;
94
+ browser?: StageBrowserSupport;
92
95
  operatorCatalog?: OperatorCatalog;
93
96
  skipGates?: boolean;
94
97
  }): Promise<PipelineRunResult>;
@@ -117,6 +120,7 @@ export declare function startPipeline(options: {
117
120
  maxActiveStagesPerRun?: number;
118
121
  executionMode?: StageExecutionMode;
119
122
  stageProcessLauncher?: StageProcessLauncher;
123
+ browser?: StageBrowserSupport;
120
124
  operatorCatalog?: OperatorCatalog;
121
125
  skipGates?: boolean;
122
126
  schedulingHalt?: {
@@ -242,6 +242,7 @@ async function preparePipeline(options) {
242
242
  hitl: options.hitl,
243
243
  executionMode,
244
244
  stageProcessLauncher,
245
+ browser: options.browser,
245
246
  operatorCatalog: options.operatorCatalog,
246
247
  skipGates: options.skipGates,
247
248
  ...(warningFindings.length > 0 ? { findings: warningFindings } : {}),
@@ -283,6 +284,7 @@ export async function runPipeline(options) {
283
284
  hitl: options.hitl,
284
285
  executionMode: options.executionMode,
285
286
  stageProcessLauncher: options.stageProcessLauncher,
287
+ browser: options.browser,
286
288
  operatorCatalog: options.operatorCatalog,
287
289
  skipGates: options.skipGates,
288
290
  });
@@ -319,6 +321,7 @@ export async function startPipeline(options) {
319
321
  hitl: options.hitl,
320
322
  executionMode: options.executionMode,
321
323
  stageProcessLauncher: options.stageProcessLauncher,
324
+ browser: options.browser,
322
325
  operatorCatalog: options.operatorCatalog,
323
326
  skipGates: options.skipGates,
324
327
  callerId: options.callerId,
@@ -1,5 +1,6 @@
1
1
  import type { AgentPort } from "../agent/port.js";
2
2
  import type { RunPipelineDagSnapshot, RunStore, StageSnapshot } from "../runstore/port.js";
3
+ import type { StageBrowserSupport } from "../browser/browserHost.js";
3
4
  import type { StageEnvelope } from "../types/envelope.js";
4
5
  import type { LoadedPipeline, ResolvedPipelineDag } from "../types/pipeline.js";
5
6
  import type { TaskFile } from "../types/task.js";
@@ -24,6 +25,7 @@ type SchedulerPreparedPipeline = {
24
25
  hitl?: StageHitlController;
25
26
  operatorCatalog?: OperatorCatalog;
26
27
  skipGates?: boolean;
28
+ browser?: StageBrowserSupport;
27
29
  };
28
30
  export type { SchedulerPreparedPipeline };
29
31
  type RetryContext = {
@@ -1,6 +1,11 @@
1
1
  import { normalizeForkChoice } from "../envelope/forkChoice.js";
2
2
  import { refreshRunDiskUsage } from "../runstore/diskUsage.js";
3
3
  import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
4
+ import { defaultStageBrowserSupport, resolveStageBrowserEnv, } from "../browser/stageBrowserEnv.js";
5
+ import { teardownRunBrowsers, teardownStageBrowser, } from "../browser/browserTeardown.js";
6
+ import { isHumanLoginStage } from "../browser/humanLogin.js";
7
+ import { createRunLiveness } from "../browser/runLiveness.js";
8
+ import { acquireStageProfile, profileWaitingMessage, } from "../browser/stageProfileLock.js";
4
9
  import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
5
10
  import { cloneFailureContinuesSchedule, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, failureIsAccepted, isCloneInstance, mint, protectedClonableChildIds, sequentialLaterCloneIds, skipRejectedNeedDependents, } from "./cloneSchedule.js";
6
11
  import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
@@ -386,6 +391,7 @@ export async function runPipelineDag(options) {
386
391
  hostEnv: process.env,
387
392
  });
388
393
  const stageById = buildStageConfigById(loaded);
394
+ const runUsesBrowser = [...stageById.values()].some((s) => s.browser !== undefined);
389
395
  const retryContext = options.retryContext;
390
396
  const feedbackSchedule = createReplaySchedule();
391
397
  await hydrateFromStore(feedbackSchedule, store, run.runId, dag);
@@ -842,6 +848,40 @@ export async function runPipelineDag(options) {
842
848
  return;
843
849
  }
844
850
  const attempt = await resolveLaunchAttempt(stageId);
851
+ if (stage.browser?.profile !== undefined) {
852
+ const profile = stage.browser.profile;
853
+ // A stage queued behind another run's lease must not hold one of this
854
+ // run's active slots; it takes the slot back once it joins the lease.
855
+ let slotReleased = false;
856
+ let acquired;
857
+ try {
858
+ acquired = await acquireStageProfile(prepared.browser ?? defaultStageBrowserSupport(), {
859
+ profile,
860
+ owner: { runId: run.runId, stageId },
861
+ isRunLive: createRunLiveness(store),
862
+ halted: () => options.schedulingHalt?.halted === true,
863
+ onWaiting: async (holder) => {
864
+ if (!slotReleased) {
865
+ slotReleased = true;
866
+ activeCount -= 1;
867
+ }
868
+ await store.appendStageEvent(run.runId, stageId, {
869
+ event: "message",
870
+ role: "host",
871
+ text: profileWaitingMessage(profile, holder),
872
+ }, { attempt });
873
+ },
874
+ });
875
+ }
876
+ finally {
877
+ if (slotReleased)
878
+ activeCount += 1;
879
+ }
880
+ if (acquired === "halted") {
881
+ states.set(stageId, "skipped");
882
+ return;
883
+ }
884
+ }
845
885
  const attemptCtx = attemptContext(attempt);
846
886
  const prep = launchFor(feedbackSchedule, stageId);
847
887
  const feedbackLoopContext = prep?.feedbackLoopContext;
@@ -899,6 +939,22 @@ export async function runPipelineDag(options) {
899
939
  // best-effort Host cleanup
900
940
  }
901
941
  };
942
+ let browserEnv;
943
+ try {
944
+ browserEnv = await resolveStageBrowserEnv(prepared.browser ?? defaultStageBrowserSupport(), {
945
+ runId: run.runId,
946
+ stageId,
947
+ runDir: run.workspaceDir,
948
+ browser: stage.browser,
949
+ attempt,
950
+ humanLogin: isHumanLoginStage(dag, definitionId, stage.browser),
951
+ });
952
+ }
953
+ catch (err) {
954
+ cleanupAttemptCredentials();
955
+ await onStageFailure(stageId, err instanceof Error ? err.message : String(err));
956
+ return;
957
+ }
902
958
  if (executionMode === "process") {
903
959
  const launcher = options.stageProcessLauncher;
904
960
  if (!launcher) {
@@ -915,8 +971,10 @@ export async function runPipelineDag(options) {
915
971
  attempt,
916
972
  env: stageBinding.env,
917
973
  bindingKind: stageBinding.kind,
974
+ model: stage.model,
918
975
  grants,
919
976
  attemptHome,
977
+ ...(browserEnv !== undefined ? { browserEnv } : {}),
920
978
  ...(sessionMode !== undefined
921
979
  ? {
922
980
  mode: sessionMode === "feedback_resume"
@@ -955,6 +1013,10 @@ export async function runPipelineDag(options) {
955
1013
  states.set(stageId, "waiting");
956
1014
  return;
957
1015
  }
1016
+ const recorded = await store.listStageEvents(run.runId, stageId, attempt);
1017
+ if (!recorded.some((event) => event.event === "failed")) {
1018
+ await store.appendStageEvent(run.runId, stageId, { event: "failed", reason: launchResult.reason }, { attempt });
1019
+ }
958
1020
  await onStageFailure(stageId, launchResult.reason);
959
1021
  return;
960
1022
  }
@@ -974,9 +1036,11 @@ export async function runPipelineDag(options) {
974
1036
  operatorCatalog: prepared.operatorCatalog,
975
1037
  completedEnvelopes,
976
1038
  skipGates: prepared.skipGates,
1039
+ ...(prepared.browser !== undefined ? { browser: prepared.browser } : {}),
977
1040
  stageEnv: {
978
1041
  ...stageBinding.env,
979
1042
  ...grants.env,
1043
+ ...browserEnv,
980
1044
  HOME: attemptHome,
981
1045
  },
982
1046
  ...(sessionMode !== undefined ? { sessionMode } : {}),
@@ -1016,7 +1080,19 @@ export async function runPipelineDag(options) {
1016
1080
  states.set(stageId, "active");
1017
1081
  launchedIds.add(stageId);
1018
1082
  activeCount += 1;
1019
- const taskPromise = launchStage(stageId).finally(() => {
1083
+ const taskPromise = launchStage(stageId)
1084
+ .then(async () => {
1085
+ const state = states.get(stageId);
1086
+ if (runUsesBrowser && (state === "succeeded" || state === "failed")) {
1087
+ await teardownStageBrowser(prepared.browser ?? defaultStageBrowserSupport(), {
1088
+ runId: run.runId,
1089
+ runDir: run.workspaceDir,
1090
+ stageId,
1091
+ events: () => store.listStageEvents(run.runId, stageId),
1092
+ }).catch(() => undefined);
1093
+ }
1094
+ })
1095
+ .finally(() => {
1020
1096
  activeCount -= 1;
1021
1097
  inFlight.delete(taskPromise);
1022
1098
  });
@@ -1084,6 +1160,15 @@ export async function runPipelineDag(options) {
1084
1160
  await Promise.all(inFlight);
1085
1161
  }
1086
1162
  const hasWaiting = [...states.values()].some((s) => s === "waiting");
1163
+ if (runUsesBrowser &&
1164
+ !(hasWaiting && !schedulingHalted) &&
1165
+ !options.schedulingHalt?.hostShutdown) {
1166
+ await teardownRunBrowsers(prepared.browser ?? defaultStageBrowserSupport(), {
1167
+ runId: run.runId,
1168
+ runDir: run.workspaceDir,
1169
+ events: (stageId) => store.listStageEvents(run.runId, stageId),
1170
+ });
1171
+ }
1087
1172
  if (hasWaiting && !schedulingHalted) {
1088
1173
  return {
1089
1174
  ok: false,
@@ -1,7 +1,7 @@
1
1
  import type { RunMeta, RunStore } from "../runstore/port.js";
2
2
  import type { TaskFile } from "../types/task.js";
3
3
  import type { WorkspaceBinding } from "./workspaceBinding.js";
4
- export type StartFailureCode = "busy_capacity" | "busy_checkout" | "busy_caller_quota" | "repository_auth_failed" | "repository_not_found" | "ref_not_found" | "repository_fetch_failed" | "worktree_create_failed" | "git_missing" | "insufficient_disk" | "disk_check_failed" | "pinned_sha_unavailable" | "invalid_run_branch" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "task.invalid_shape" | "task.load_error" | "start.token_rejected" | "inline_pipeline_too_large" | "skills_invalid_name" | "skills_invalid_path" | "skills_missing_skill_md" | "skills_payload_too_large" | "missing_tool" | "tool_version_mismatch" | "unknown_version" | "secret_unavailable" | "unresolved_var" | "shutting_down" | "autostart_disabled" | "ensure_project_not_allowed" | "claude_as_root";
4
+ export type StartFailureCode = "busy_capacity" | "busy_checkout" | "busy_caller_quota" | "repository_auth_failed" | "repository_not_found" | "ref_not_found" | "repository_fetch_failed" | "worktree_create_failed" | "git_missing" | "insufficient_disk" | "disk_check_failed" | "pinned_sha_unavailable" | "invalid_run_branch" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "task.invalid_shape" | "task.load_error" | "trigger.task_override_not_allowed" | "trigger.task_required" | "start.token_rejected" | "inline_pipeline_too_large" | "skills_invalid_name" | "skills_invalid_path" | "skills_missing_skill_md" | "skills_payload_too_large" | "missing_tool" | "tool_version_mismatch" | "unknown_version" | "secret_unavailable" | "unresolved_var" | "shutting_down" | "autostart_disabled" | "ensure_project_not_allowed" | "claude_as_root";
5
5
  export type MaterializedBinding = {
6
6
  runId: string;
7
7
  checkoutRoot?: string;
@@ -9,6 +9,7 @@ import { type SkillsPayload } from "./runSkills.js";
9
9
  import { type FeedbackLoopDecisionKind } from "./feedbackLoopDecision.js";
10
10
  import { type RetryStageResult } from "./runRetryCoordinator.js";
11
11
  import { type StageExecutionMode } from "./stageConcurrency.js";
12
+ import type { StageBrowserSupport } from "../browser/browserHost.js";
12
13
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
13
14
  import { StageHitlController, type DeliverAnswerResult, type HitlSeams } from "./stageHitl.js";
14
15
  import { type StartTaskInput } from "./taskInput.js";
@@ -190,6 +191,7 @@ export declare class RunManager {
190
191
  maxActiveStagesPerRun?: number;
191
192
  executionMode?: StageExecutionMode;
192
193
  stageProcessLauncher?: StageProcessLauncher;
194
+ browser?: StageBrowserSupport;
193
195
  a2aStore?: A2aStore;
194
196
  knownWritableProjectRoots?: () => Iterable<string> | Promise<Iterable<string>>;
195
197
  freeSpaceReader?: FreeSpaceReader;
@@ -235,6 +237,9 @@ export declare class RunManager {
235
237
  stageId: string;
236
238
  }>>;
237
239
  hasActiveWorker(runId: string, stageId: string): boolean;
240
+ sweepBrowserSessions(): Promise<{
241
+ closed: string[];
242
+ }>;
238
243
  reconcileOrphanedStages(): Promise<{
239
244
  reconciled: Array<{
240
245
  runId: string;
@@ -344,6 +349,12 @@ export declare class RunManager {
344
349
  reason?: string;
345
350
  }): Promise<DecideFeedbackLoopResult>;
346
351
  private resumeViaSubprocess;
352
+ /**
353
+ * A resumed stage whose after-phase verification failed is repaired by the
354
+ * same policy the scheduler applies to a first-launch failure. Returns the
355
+ * freshly created attempt, or undefined when the failure is final.
356
+ */
357
+ private scheduleRepairAfterVerifyFailure;
347
358
  private reconstructAndContinue;
348
359
  private reserveAndStartPipeline;
349
360
  private busyFailure;