stageflow 0.42.0 → 0.46.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 (315) hide show
  1. package/README.md +12 -3
  2. package/dist/agent/activity.d.ts +18 -0
  3. package/dist/agent/claudeAdapter.js +7 -0
  4. package/dist/agent/claudeTools.d.ts +27 -18
  5. package/dist/agent/claudeTools.js +54 -15
  6. package/dist/agent/compactionEvents.d.ts +34 -0
  7. package/dist/agent/compactionEvents.js +37 -0
  8. package/dist/agent/openRouterRouting.d.ts +7 -0
  9. package/dist/agent/openRouterRouting.js +23 -0
  10. package/dist/agent/piAdapter.d.ts +6 -0
  11. package/dist/agent/piAdapter.js +128 -27
  12. package/dist/agent/piSettings.d.ts +7 -0
  13. package/dist/agent/piSettings.js +9 -0
  14. package/dist/agent/sandboxPiTools.d.ts +3 -1
  15. package/dist/agent/sandboxPiTools.js +8 -2
  16. package/dist/agent/stageTurnDriver.d.ts +11 -2
  17. package/dist/agent/stageTurnDriver.js +45 -4
  18. package/dist/agent/toolOutputBudget.d.ts +15 -1
  19. package/dist/agent/toolOutputBudget.js +18 -0
  20. package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
  21. package/dist/agent/toolOutputBudgetExtension.js +40 -3
  22. package/dist/agents/authorChat.d.ts +1 -0
  23. package/dist/agents/authorChat.js +7 -0
  24. package/dist/agents/authorSessionStore.d.ts +3 -0
  25. package/dist/agents/authorSessionStore.js +17 -2
  26. package/dist/agents/evalGate.js +1 -0
  27. package/dist/agents/hostContext.d.ts +1 -1
  28. package/dist/agents/runtime/piRuntime.d.ts +5 -1
  29. package/dist/agents/runtime/piRuntime.js +46 -12
  30. package/dist/agents/service.js +3 -2
  31. package/dist/agents/sessions.js +3 -1
  32. package/dist/cli/evalCommand.d.ts +1 -1
  33. package/dist/cli/evalCommand.js +39 -2
  34. package/dist/cli/hostClient.d.ts +6 -0
  35. package/dist/cli/hostClient.js +4 -0
  36. package/dist/compaction/directives.d.ts +18 -0
  37. package/dist/compaction/directives.js +119 -0
  38. package/dist/compaction/extension.d.ts +20 -0
  39. package/dist/compaction/extension.js +78 -0
  40. package/dist/compaction/mask.d.ts +18 -0
  41. package/dist/compaction/mask.js +155 -0
  42. package/dist/compaction/pin.d.ts +21 -0
  43. package/dist/compaction/pin.js +270 -0
  44. package/dist/compaction/scrub.d.ts +7 -0
  45. package/dist/compaction/scrub.js +26 -0
  46. package/dist/compaction/sessionWiring.d.ts +22 -0
  47. package/dist/compaction/sessionWiring.js +26 -0
  48. package/dist/compaction/summarize.d.ts +79 -0
  49. package/dist/compaction/summarize.js +171 -0
  50. package/dist/compaction/toolPolicy.d.ts +3 -0
  51. package/dist/compaction/toolPolicy.js +33 -0
  52. package/dist/compaction/types.d.ts +45 -0
  53. package/dist/compaction/types.js +7 -0
  54. package/dist/compaction/workingState.d.ts +56 -0
  55. package/dist/compaction/workingState.js +198 -0
  56. package/dist/config/createPipeline.js +2 -0
  57. package/dist/config/createTrigger.js +7 -1
  58. package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
  59. package/dist/config/ensureCatalogScanRoot.js +6 -0
  60. package/dist/config/evalCatalog.js +30 -2
  61. package/dist/config/githubTriggerAction.d.ts +8 -0
  62. package/dist/config/githubTriggerAction.js +29 -0
  63. package/dist/config/hostConfig.js +2 -0
  64. package/dist/config/loadEval.d.ts +3 -1
  65. package/dist/config/loadEval.js +96 -10
  66. package/dist/config/pipelineNeeds.d.ts +2 -0
  67. package/dist/config/pipelineNeeds.js +16 -3
  68. package/dist/config/pipelineRoute.js +5 -4
  69. package/dist/config/validateCatalog.d.ts +1 -1
  70. package/dist/config/validateCatalog.js +12 -0
  71. package/dist/config/validateEval.js +60 -3
  72. package/dist/config/writeEval.js +2 -2
  73. package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
  74. package/dist/envelope/assertEmittedArtifacts.js +43 -0
  75. package/dist/envelope/preEmitChecks.d.ts +3 -0
  76. package/dist/evals/calibration.d.ts +21 -0
  77. package/dist/evals/calibration.js +69 -0
  78. package/dist/evals/calibrationSuggest.d.ts +29 -0
  79. package/dist/evals/calibrationSuggest.js +72 -0
  80. package/dist/evals/calibrator.d.ts +46 -0
  81. package/dist/evals/calibrator.js +148 -0
  82. package/dist/evals/compare.d.ts +1 -1
  83. package/dist/evals/compare.js +3 -0
  84. package/dist/evals/constants.d.ts +7 -3
  85. package/dist/evals/constants.js +7 -1
  86. package/dist/evals/engine/FakeEngine.d.ts +2 -1
  87. package/dist/evals/engine/FakeEngine.js +9 -3
  88. package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
  89. package/dist/evals/engine/promptfoo/PromptfooEngine.js +4 -2
  90. package/dist/evals/engine/promptfoo/generate.d.ts +3 -1
  91. package/dist/evals/engine/promptfoo/generate.js +43 -23
  92. package/dist/evals/engine/promptfoo/judgeProviders.js +2 -1
  93. package/dist/evals/engine/promptfoo/normalize.d.ts +4 -1
  94. package/dist/evals/engine/promptfoo/normalize.js +16 -3
  95. package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +3 -1
  96. package/dist/evals/engine/promptfoo/rubricPrompt.js +15 -5
  97. package/dist/evals/errors.d.ts +7 -0
  98. package/dist/evals/errors.js +10 -0
  99. package/dist/evals/judgeProviders.d.ts +6 -0
  100. package/dist/evals/judgeProviders.js +14 -0
  101. package/dist/evals/judged.d.ts +7 -0
  102. package/dist/evals/judged.js +7 -0
  103. package/dist/evals/manager.d.ts +27 -8
  104. package/dist/evals/manager.js +73 -12
  105. package/dist/evals/plan.d.ts +10 -1
  106. package/dist/evals/plan.js +33 -13
  107. package/dist/evals/planInternalAgent.d.ts +19 -0
  108. package/dist/evals/planInternalAgent.js +201 -0
  109. package/dist/evals/read/api.d.ts +18 -0
  110. package/dist/evals/read/api.js +10 -1
  111. package/dist/evals/read/labelQueue.d.ts +45 -0
  112. package/dist/evals/read/labelQueue.js +83 -0
  113. package/dist/evals/read/list.js +10 -1
  114. package/dist/evals/read/runs.d.ts +5 -0
  115. package/dist/evals/read/runs.js +7 -1
  116. package/dist/evals/runner/caseRunner.d.ts +2 -1
  117. package/dist/evals/runner/caseRunner.js +16 -5
  118. package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
  119. package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
  120. package/dist/evals/snapshot.d.ts +10 -2
  121. package/dist/evals/snapshot.js +8 -3
  122. package/dist/evals/stats.d.ts +40 -0
  123. package/dist/evals/stats.js +69 -0
  124. package/dist/evals/storeTypes.d.ts +42 -0
  125. package/dist/evals/types.d.ts +25 -2
  126. package/dist/evals/types.js +4 -1
  127. package/dist/evals/votes.d.ts +10 -0
  128. package/dist/evals/votes.js +60 -0
  129. package/dist/guide/cardContract.d.ts +133 -0
  130. package/dist/guide/cardContract.js +156 -0
  131. package/dist/guide/chat.d.ts +45 -0
  132. package/dist/guide/chat.js +378 -0
  133. package/dist/guide/knowledge/index.d.ts +14 -0
  134. package/dist/guide/knowledge/index.js +109 -0
  135. package/dist/guide/knowledge/topics/agents.d.ts +2 -0
  136. package/dist/guide/knowledge/topics/agents.js +58 -0
  137. package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
  138. package/dist/guide/knowledge/topics/bestPractices.js +67 -0
  139. package/dist/guide/knowledge/topics/connections.d.ts +2 -0
  140. package/dist/guide/knowledge/topics/connections.js +66 -0
  141. package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
  142. package/dist/guide/knowledge/topics/consoleMap.js +55 -0
  143. package/dist/guide/knowledge/topics/evals.d.ts +2 -0
  144. package/dist/guide/knowledge/topics/evals.js +72 -0
  145. package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
  146. package/dist/guide/knowledge/topics/limitations.js +84 -0
  147. package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
  148. package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
  149. package/dist/guide/knowledge/topics/memories.d.ts +2 -0
  150. package/dist/guide/knowledge/topics/memories.js +63 -0
  151. package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
  152. package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
  153. package/dist/guide/knowledge/topics/overview.d.ts +2 -0
  154. package/dist/guide/knowledge/topics/overview.js +56 -0
  155. package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
  156. package/dist/guide/knowledge/topics/pipelines.js +93 -0
  157. package/dist/guide/knowledge/topics/stages.d.ts +2 -0
  158. package/dist/guide/knowledge/topics/stages.js +54 -0
  159. package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
  160. package/dist/guide/knowledge/topics/tasks.js +52 -0
  161. package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
  162. package/dist/guide/knowledge/topics/triggers.js +68 -0
  163. package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
  164. package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
  165. package/dist/guide/knowledgeTools.d.ts +2 -0
  166. package/dist/guide/knowledgeTools.js +56 -0
  167. package/dist/guide/showUi.d.ts +6 -0
  168. package/dist/guide/showUi.js +546 -0
  169. package/dist/guide/threadStore.d.ts +48 -0
  170. package/dist/guide/threadStore.js +189 -0
  171. package/dist/guide/writeRoot.d.ts +11 -0
  172. package/dist/guide/writeRoot.js +70 -0
  173. package/dist/hosted/translate.js +2 -0
  174. package/dist/mcp/agentTools.js +12 -0
  175. package/dist/mcp/catalogTools.js +18 -20
  176. package/dist/mcp/catalogWriteTools.d.ts +8 -0
  177. package/dist/mcp/catalogWriteTools.js +225 -0
  178. package/dist/mcp/connectionTools.d.ts +3 -0
  179. package/dist/mcp/connectionTools.js +85 -0
  180. package/dist/mcp/controlTools.js +34 -1
  181. package/dist/mcp/deps.d.ts +9 -0
  182. package/dist/mcp/evalTools.js +723 -21
  183. package/dist/mcp/memoryHostTools.d.ts +3 -0
  184. package/dist/mcp/memoryHostTools.js +72 -0
  185. package/dist/mcp/pipelineAuthorTools.js +14 -1
  186. package/dist/mcp/server.js +5 -0
  187. package/dist/mcp/tools.js +6 -0
  188. package/dist/mcp/triggerTools.js +77 -31
  189. package/dist/operatorAgent/evalGate.d.ts +22 -0
  190. package/dist/operatorAgent/evalGate.js +77 -0
  191. package/dist/operatorAgent/fakeHost.js +10 -2
  192. package/dist/operatorAgent/host.js +18 -2
  193. package/dist/operatorAgent/index.d.ts +1 -0
  194. package/dist/operatorAgent/index.js +1 -0
  195. package/dist/operatorAgent/internalAgents.d.ts +11 -0
  196. package/dist/operatorAgent/internalAgents.js +13 -0
  197. package/dist/operatorAgent/piModel.d.ts +5 -1
  198. package/dist/operatorAgent/piModel.js +15 -10
  199. package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
  200. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
  201. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
  202. package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
  203. package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
  204. package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
  205. package/dist/operatorAgent/profiles/guideTools.js +123 -0
  206. package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
  207. package/dist/operatorAgent/profiles/stageflowGuide.js +133 -0
  208. package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
  209. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  210. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
  211. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
  212. package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
  213. package/dist/operatorAgent/types.d.ts +23 -0
  214. package/dist/package-meta.d.ts +1 -1
  215. package/dist/package-meta.js +1 -1
  216. package/dist/runstore/port.d.ts +13 -1
  217. package/dist/runstore/retention.d.ts +2 -1
  218. package/dist/runstore/retention.js +3 -7
  219. package/dist/runstore/runStatusClock.d.ts +17 -0
  220. package/dist/runstore/runStatusClock.js +19 -0
  221. package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
  222. package/dist/runstore/sqlite/SqliteRunStore.js +70 -45
  223. package/dist/runstore/sqlite/calibrationStore.d.ts +10 -0
  224. package/dist/runstore/sqlite/calibrationStore.js +106 -0
  225. package/dist/runstore/sqlite/evalStore.d.ts +5 -0
  226. package/dist/runstore/sqlite/evalStore.js +7 -0
  227. package/dist/runstore/sqlite/migrations/018-eval-grader-labels.d.ts +11 -0
  228. package/dist/runstore/sqlite/migrations/018-eval-grader-labels.js +43 -0
  229. package/dist/runstore/sqlite/migrations/index.js +2 -0
  230. package/dist/runstore/workspaceLayout.js +6 -1
  231. package/dist/runtime/artifactDiskInspect.d.ts +14 -0
  232. package/dist/runtime/artifactDiskInspect.js +52 -0
  233. package/dist/runtime/artifactPath.d.ts +24 -0
  234. package/dist/runtime/artifactPath.js +84 -0
  235. package/dist/runtime/completionCheckRunner.js +15 -43
  236. package/dist/runtime/githubPollSource.d.ts +22 -4
  237. package/dist/runtime/githubPollSource.js +93 -8
  238. package/dist/runtime/priorArtifactGrants.d.ts +8 -0
  239. package/dist/runtime/priorArtifactGrants.js +105 -0
  240. package/dist/runtime/runActionEligibility.d.ts +46 -0
  241. package/dist/runtime/runActionEligibility.js +123 -0
  242. package/dist/runtime/runManager.d.ts +4 -1
  243. package/dist/runtime/runManager.js +52 -10
  244. package/dist/runtime/runRetryCoordinator.d.ts +2 -14
  245. package/dist/runtime/runRetryCoordinator.js +3 -67
  246. package/dist/runtime/sandboxContainer.d.ts +7 -0
  247. package/dist/runtime/sandboxContainer.js +48 -6
  248. package/dist/runtime/sandboxDesk.d.ts +2 -1
  249. package/dist/runtime/sandboxDesk.js +11 -12
  250. package/dist/runtime/stageEnvironment.js +1 -0
  251. package/dist/runtime/stageRoots.d.ts +12 -0
  252. package/dist/runtime/stageSandbox.d.ts +2 -1
  253. package/dist/runtime/stageSandbox.js +18 -2
  254. package/dist/runtime/stageWorker.js +20 -0
  255. package/dist/server/agentAuthorRoutes.js +12 -3
  256. package/dist/server/bootstrap.d.ts +5 -0
  257. package/dist/server/bootstrap.js +14 -1
  258. package/dist/server/evalRoutes.js +251 -12
  259. package/dist/server/guideRoutes.d.ts +5 -0
  260. package/dist/server/guideRoutes.js +166 -0
  261. package/dist/server/http.d.ts +10 -0
  262. package/dist/server/http.js +35 -1
  263. package/dist/server/internalAgentRoutes.d.ts +6 -0
  264. package/dist/server/internalAgentRoutes.js +12 -0
  265. package/dist/server/mcpHost.js +2 -0
  266. package/dist/toolkit/meta.d.ts +1 -1
  267. package/dist/toolkit/meta.js +41 -1
  268. package/dist/toolkit/profiles.d.ts +5 -0
  269. package/dist/toolkit/profiles.js +12 -0
  270. package/dist/tools/askOperator.js +2 -1
  271. package/dist/tools/emitStageEnvelope.d.ts +17 -16
  272. package/dist/tools/emitStageEnvelope.js +46 -32
  273. package/dist/tools/stageToolNames.d.ts +4 -0
  274. package/dist/tools/stageToolNames.js +4 -0
  275. package/dist/tools/workingState.d.ts +40 -0
  276. package/dist/tools/workingState.js +108 -0
  277. package/dist/tools/writeStageArtifact.d.ts +0 -9
  278. package/dist/tools/writeStageArtifact.js +2 -1
  279. package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-BS2BBbCx.js} +2 -2
  280. package/dist/ui/assets/adapters-DPD-cAsy.js +17 -0
  281. package/dist/ui/assets/chat-BD1-VTT6.js +2 -0
  282. package/dist/ui/assets/chat-DXnQv9st.css +2 -0
  283. package/dist/ui/assets/main-AX5XNLtX.js +278 -0
  284. package/dist/ui/assets/{main-CHlmTNJe.css → main-CFX0_n8J.css} +1 -1
  285. package/dist/ui/chat.html +3 -4
  286. package/dist/ui/index.html +3 -4
  287. package/dist/workshop/chatTurn.js +1 -1
  288. package/dist/workshop/workshopPin.d.ts +3 -3
  289. package/dist/workshop/workshopPin.js +16 -15
  290. package/examples/compaction-stress/README.md +30 -0
  291. package/examples/compaction-stress/check-run.mjs +53 -0
  292. package/examples/compaction-stress/collect-evidence.yaml +60 -0
  293. package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
  294. package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
  295. package/examples/compaction-stress/generate-evidence.mjs +110 -0
  296. package/examples/compaction-stress/root-cause.yaml +26 -0
  297. package/examples/compaction-stress/stageflow.yaml +9 -0
  298. package/examples/compaction-stress/write-postmortem.yaml +40 -0
  299. package/examples/evals/README.md +18 -0
  300. package/examples/evals/guide-behavior.dataset.jsonl +15 -0
  301. package/examples/evals/guide-behavior.eval.yaml +35 -0
  302. package/examples/evals/guide-depth.dataset.jsonl +35 -0
  303. package/examples/evals/guide-depth.eval.yaml +169 -0
  304. package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
  305. package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
  306. package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
  307. package/examples/memory/evals/guide-depth.eval.yaml +169 -0
  308. package/package.json +1 -1
  309. package/skills/stageflow/references/control-surface.md +1 -1
  310. package/skills/stageflow/references/evals.md +1 -0
  311. package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
  312. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
  313. package/dist/ui/assets/chat-Df_wCpak.js +0 -5
  314. package/dist/ui/assets/chat-P7KQznHm.css +0 -2
  315. package/dist/ui/assets/main-CvVfxmMd.js +0 -271
@@ -0,0 +1,84 @@
1
+ import path from "node:path";
2
+ export function attemptArtifactsPrefix(stageId, attempt) {
3
+ return path.posix.join("stages", stageId, "attempts", String(attempt), "artifacts");
4
+ }
5
+ export function buildArtifactListedPath(parts) {
6
+ const relative = parts.relative.replace(/\\/g, "/");
7
+ const segments = relative.split("/").filter((segment) => segment.length > 0);
8
+ return path.posix.join("stages", parts.stageId, "attempts", String(parts.attempt), "artifacts", ...segments);
9
+ }
10
+ function parseStrictGrantable(listedPath) {
11
+ if (typeof listedPath !== "string" || listedPath.length === 0)
12
+ return undefined;
13
+ const posix = listedPath.replace(/\\/g, "/");
14
+ if (path.isAbsolute(listedPath) || path.posix.isAbsolute(posix))
15
+ return undefined;
16
+ if (posix.includes("\0") || posix.includes(".."))
17
+ return undefined;
18
+ const parts = posix.split("/").filter((part) => part.length > 0);
19
+ if (parts.length < 6)
20
+ return undefined;
21
+ if (parts[0] !== "stages" || parts[2] !== "attempts" || parts[4] !== "artifacts") {
22
+ return undefined;
23
+ }
24
+ const attemptRaw = parts[3];
25
+ if (!/^\d+$/.test(attemptRaw))
26
+ return undefined;
27
+ const attempt = Number(attemptRaw);
28
+ if (!Number.isInteger(attempt) || attempt < 1)
29
+ return undefined;
30
+ const relative = parts.slice(5).join("/");
31
+ if (relative.length === 0)
32
+ return undefined;
33
+ return {
34
+ stageId: parts[1],
35
+ attempt,
36
+ relative,
37
+ };
38
+ }
39
+ /**
40
+ * Accept a listed artifact path under a mode.
41
+ * - `grantable`: full run-relative path to a prior stage/attempt artifact file.
42
+ * - `current_attempt`: that attempt's run-relative prefix, or a bare path under
43
+ * its artifacts dir; foreign `stages/...` paths are rejected.
44
+ */
45
+ export function acceptArtifactListedPath(listedPath, mode) {
46
+ if (mode.kind === "grantable") {
47
+ return parseStrictGrantable(listedPath);
48
+ }
49
+ if (typeof listedPath !== "string" || listedPath.length === 0) {
50
+ return undefined;
51
+ }
52
+ const posixListed = listedPath.replace(/\\/g, "/");
53
+ if (path.isAbsolute(listedPath) || path.posix.isAbsolute(posixListed)) {
54
+ return undefined;
55
+ }
56
+ const prefix = attemptArtifactsPrefix(mode.stageId, mode.attempt);
57
+ if (posixListed === prefix || posixListed === `${prefix}/`) {
58
+ return undefined;
59
+ }
60
+ if (posixListed.startsWith(`${prefix}/`)) {
61
+ const relative = posixListed.slice(prefix.length + 1);
62
+ if (relative.length === 0)
63
+ return undefined;
64
+ return {
65
+ stageId: mode.stageId,
66
+ attempt: mode.attempt,
67
+ relative,
68
+ };
69
+ }
70
+ if (posixListed.startsWith("stages/")) {
71
+ return undefined;
72
+ }
73
+ return {
74
+ stageId: mode.stageId,
75
+ attempt: mode.attempt,
76
+ relative: posixListed,
77
+ };
78
+ }
79
+ export function parseArtifactListedPath(listedPath) {
80
+ return parseStrictGrantable(listedPath);
81
+ }
82
+ export function isGrantableArtifactPath(listedPath) {
83
+ return acceptArtifactListedPath(listedPath, { kind: "grantable" }) !== undefined;
84
+ }
@@ -1,11 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { lstat, readFile } from "node:fs/promises";
3
+ import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
6
6
  import { resolveBashPath } from "../preflight/bash.js";
7
7
  import { redactString } from "../logging/redact.js";
8
8
  import { getNamedSecrets } from "../logging/namedSecrets.js";
9
+ import { inspectArtifactUnderRoot } from "./artifactDiskInspect.js";
9
10
  import { signalProcessGroup } from "./stageProcessLauncher.js";
10
11
  const MAX_COMMAND_OUTPUT_BYTES = 64 * 1024;
11
12
  const COMMAND_KILL_ESCALATE_MS = 2000;
@@ -163,56 +164,27 @@ async function runCommandCheck(check, input) {
163
164
  }
164
165
  }
165
166
  async function inspectArtifact(artifactsDir, relativePath) {
166
- const target = path.resolve(artifactsDir, relativePath);
167
- const relative = path.relative(artifactsDir, target);
168
- if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
167
+ const presence = await inspectArtifactUnderRoot(artifactsDir, relativePath);
168
+ if (presence.file_type === "missing") {
169
169
  return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
170
170
  }
171
- try {
172
- // lstat every component rather than just the target: lstat(target) follows
173
- // a symlink in a parent directory, which could otherwise prove a file that
174
- // lives outside the attempt artifact root.
175
- const segments = relative.split(path.sep).filter((segment) => segment !== "");
176
- let current = artifactsDir;
177
- let stats;
178
- for (const segment of segments) {
179
- current = path.join(current, segment);
180
- stats = await lstat(current);
181
- if (stats.isSymbolicLink()) {
182
- return { kind: "artifact", path: relativePath, exists: true, file_type: "symlink" };
183
- }
184
- if (current !== target && !stats.isDirectory()) {
185
- return { kind: "artifact", path: relativePath, exists: true, file_type: "other" };
186
- }
187
- }
188
- if (stats === undefined) {
189
- return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
190
- }
191
- if (stats.isSymbolicLink()) {
192
- return { kind: "artifact", path: relativePath, exists: true, file_type: "symlink" };
193
- }
194
- if (stats.isDirectory()) {
195
- return { kind: "artifact", path: relativePath, exists: true, file_type: "directory" };
196
- }
197
- if (!stats.isFile()) {
198
- return { kind: "artifact", path: relativePath, exists: true, file_type: "other" };
199
- }
200
- const content = await readFile(target);
171
+ if (presence.file_type !== "file" || presence.absolutePath === undefined) {
201
172
  return {
202
173
  kind: "artifact",
203
174
  path: relativePath,
204
175
  exists: true,
205
- file_type: "file",
206
- size_bytes: stats.size,
207
- sha256: createHash("sha256").update(content).digest("hex"),
176
+ file_type: presence.file_type,
208
177
  };
209
178
  }
210
- catch (error) {
211
- if (error.code === "ENOENT") {
212
- return { kind: "artifact", path: relativePath, exists: false, file_type: "missing" };
213
- }
214
- throw error;
215
- }
179
+ const content = await readFile(presence.absolutePath);
180
+ return {
181
+ kind: "artifact",
182
+ path: relativePath,
183
+ exists: true,
184
+ file_type: "file",
185
+ size_bytes: presence.size_bytes,
186
+ sha256: createHash("sha256").update(content).digest("hex"),
187
+ };
216
188
  }
217
189
  async function runArtifactCheck(check, input) {
218
190
  try {
@@ -15,6 +15,7 @@ export type GithubPullItem = {
15
15
  user: {
16
16
  login: string;
17
17
  } | null;
18
+ head_sha?: string | null;
18
19
  };
19
20
  export type GithubPollResponse = {
20
21
  status: number;
@@ -35,14 +36,28 @@ export type GithubPollClient = {
35
36
  };
36
37
  export declare function defaultCreateGithubClient(token: string): GithubPollClient;
37
38
  export type NormalizedGithubEvent = {
38
- action: "opened" | "merged" | "closed" | "updated";
39
+ action: "opened" | "synchronize" | "updated" | "merged" | "closed";
39
40
  number: number;
40
41
  title: string;
41
42
  url: string;
42
43
  author: string;
43
44
  repo: string;
44
45
  updatedAt: string;
46
+ headSha?: string;
45
47
  };
48
+ type PollCursor = {
49
+ updatedAt: string;
50
+ number: number;
51
+ };
52
+ /**
53
+ * Classify a PR list delta into a normalized GitHub action.
54
+ *
55
+ * `opened` is reserved for brand-new PRs: equal created/updated timestamps, or
56
+ * first sight of a number whose `created_at` is after the poll cursor. A missing
57
+ * prior head SHA alone is not enough — that also happens on upgrade (no `heads`
58
+ * row yet), eviction, or a cold PR falling into the poll window.
59
+ */
60
+ export declare function deriveGithubAction(item: GithubPullItem, previousHeadSha: string | undefined, cursor?: PollCursor | null): NormalizedGithubEvent["action"];
46
61
  export type GithubPollSourceOptions = {
47
62
  store: RunStore;
48
63
  cwd?: string;
@@ -59,9 +74,11 @@ export type GithubPollSourceOptions = {
59
74
  * per tick regardless of how many triggers watch it), and diffs each repo's
60
75
  * PR list against a persisted ETag + cursor to find genuinely new activity.
61
76
  *
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.
77
+ * The first tick for a never-before-seen repo only seeds the cursor and
78
+ * per-PR head SHAs — it never fires for a repo's pre-existing PR history,
79
+ * mirroring how `ScheduleSource` seeds `next_run_at` without firing on
80
+ * first sight. The same quiet baseline runs when a cursor already exists
81
+ * but `heads` has never been written (upgrade from a pre-heads store).
65
82
  */
66
83
  export declare class GithubPollSource implements TriggerSourcePort {
67
84
  private readonly store;
@@ -84,3 +101,4 @@ export declare class GithubPollSource implements TriggerSourcePort {
84
101
  private pollRepo;
85
102
  private fireSafely;
86
103
  }
104
+ export {};
@@ -50,31 +50,99 @@ export function defaultCreateGithubClient(token) {
50
50
  created_at: pr.created_at,
51
51
  updated_at: pr.updated_at,
52
52
  user: pr.user ? { login: pr.user.login } : null,
53
+ head_sha: pr.head?.sha ?? null,
53
54
  })),
54
55
  };
55
56
  },
56
57
  };
57
58
  }
58
- function deriveAction(item) {
59
+ const HEADS_LIMIT = 200;
60
+ function isCreatedAfterCursor(item, cursor) {
61
+ const createdMs = new Date(item.created_at).getTime();
62
+ const cursorMs = new Date(cursor.updatedAt).getTime();
63
+ if (createdMs !== cursorMs)
64
+ return createdMs > cursorMs;
65
+ return item.number > cursor.number;
66
+ }
67
+ /**
68
+ * Classify a PR list delta into a normalized GitHub action.
69
+ *
70
+ * `opened` is reserved for brand-new PRs: equal created/updated timestamps, or
71
+ * first sight of a number whose `created_at` is after the poll cursor. A missing
72
+ * prior head SHA alone is not enough — that also happens on upgrade (no `heads`
73
+ * row yet), eviction, or a cold PR falling into the poll window.
74
+ */
75
+ export function deriveGithubAction(item, previousHeadSha, cursor = null) {
59
76
  if (item.merged_at)
60
77
  return "merged";
61
78
  if (item.state === "closed")
62
79
  return "closed";
63
80
  if (item.created_at === item.updated_at)
64
81
  return "opened";
82
+ if (previousHeadSha === undefined) {
83
+ if (cursor !== null && isCreatedAfterCursor(item, cursor))
84
+ return "opened";
85
+ return "updated";
86
+ }
87
+ const currentSha = item.head_sha ?? null;
88
+ if (previousHeadSha !== "" &&
89
+ currentSha !== null &&
90
+ currentSha !== previousHeadSha) {
91
+ return "synchronize";
92
+ }
65
93
  return "updated";
66
94
  }
67
- function normalizeGithubItem(item, repo) {
95
+ function normalizeGithubItem(item, repo, previousHeadSha, cursor) {
96
+ const headSha = item.head_sha ?? null;
68
97
  return {
69
- action: deriveAction(item),
98
+ action: deriveGithubAction(item, previousHeadSha, cursor),
70
99
  number: item.number,
71
100
  title: item.title,
72
101
  url: item.html_url,
73
102
  author: item.user?.login ?? "unknown",
74
103
  repo,
75
104
  updatedAt: item.updated_at,
105
+ ...(headSha ? { headSha } : {}),
76
106
  };
77
107
  }
108
+ function parseHeads(raw) {
109
+ if (raw === null)
110
+ return {};
111
+ try {
112
+ const parsed = JSON.parse(raw);
113
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
114
+ return {};
115
+ const out = {};
116
+ for (const [key, value] of Object.entries(parsed)) {
117
+ if (typeof value === "string")
118
+ out[key] = value;
119
+ }
120
+ return out;
121
+ }
122
+ catch {
123
+ return {};
124
+ }
125
+ }
126
+ function seedHeadsFromItems(items) {
127
+ const heads = {};
128
+ for (const item of items) {
129
+ const sha = item.head_sha ?? null;
130
+ if (sha)
131
+ heads[String(item.number)] = sha;
132
+ }
133
+ return heads;
134
+ }
135
+ function boundHeads(heads, currentItems) {
136
+ if (Object.keys(heads).length <= HEADS_LIMIT)
137
+ return heads;
138
+ const currentNumbers = new Set(currentItems.map((item) => String(item.number)));
139
+ const next = {};
140
+ for (const [key, value] of Object.entries(heads)) {
141
+ if (currentNumbers.has(key))
142
+ next[key] = value;
143
+ }
144
+ return next;
145
+ }
78
146
  function parseCursor(raw) {
79
147
  if (raw === null)
80
148
  return null;
@@ -111,9 +179,11 @@ function firstSecretRef(matches) {
111
179
  * per tick regardless of how many triggers watch it), and diffs each repo's
112
180
  * PR list against a persisted ETag + cursor to find genuinely new activity.
113
181
  *
114
- * The first tick for a never-before-seen repo only seeds the cursor — it
115
- * never fires for a repo's pre-existing PR history, mirroring how
116
- * `ScheduleSource` seeds `next_run_at` without firing on first sight.
182
+ * The first tick for a never-before-seen repo only seeds the cursor and
183
+ * per-PR head SHAs — it never fires for a repo's pre-existing PR history,
184
+ * mirroring how `ScheduleSource` seeds `next_run_at` without firing on
185
+ * first sight. The same quiet baseline runs when a cursor already exists
186
+ * but `heads` has never been written (upgrade from a pre-heads store).
117
187
  */
118
188
  export class GithubPollSource {
119
189
  store;
@@ -217,6 +287,8 @@ export class GithubPollSource {
217
287
  const adapterId = `github:${repo}`;
218
288
  const etag = await this.store.getTriggerAdapterState(adapterId, "etag");
219
289
  const cursor = parseCursor(await this.store.getTriggerAdapterState(adapterId, "cursor"));
290
+ const headsRaw = await this.store.getTriggerAdapterState(adapterId, "heads");
291
+ let heads = parseHeads(headsRaw);
220
292
  const [owner, repoName] = repo.split("/");
221
293
  let response;
222
294
  try {
@@ -239,13 +311,24 @@ export class GithubPollSource {
239
311
  if (response.items.length === 0)
240
312
  return;
241
313
  const newest = response.items[0];
242
- if (cursor === null) {
314
+ // First sight, or upgrade from a pre-heads store (cursor present, heads absent):
315
+ // baseline without firing so aged PRs are not misclassified as opened.
316
+ if (cursor === null || headsRaw === null) {
317
+ heads = seedHeadsFromItems(response.items);
318
+ await this.store.setTriggerAdapterState(adapterId, "heads", JSON.stringify(heads));
243
319
  await this.store.setTriggerAdapterState(adapterId, "cursor", JSON.stringify({ updatedAt: newest.updated_at, number: newest.number }));
244
320
  return;
245
321
  }
246
322
  const newItems = response.items.filter((item) => isNewerThanCursor(item, cursor)).reverse();
247
323
  for (const item of newItems) {
248
- const summary = normalizeGithubItem(item, repo);
324
+ const numberKey = String(item.number);
325
+ const previousHeadSha = Object.prototype.hasOwnProperty.call(heads, numberKey) ? heads[numberKey] : undefined;
326
+ const summary = normalizeGithubItem(item, repo, previousHeadSha, cursor);
327
+ const nextSha = item.head_sha ?? null;
328
+ if (nextSha)
329
+ heads[numberKey] = nextSha;
330
+ else if (previousHeadSha !== undefined && previousHeadSha !== "")
331
+ heads[numberKey] = previousHeadSha;
249
332
  for (const match of matches) {
250
333
  const { definition } = match;
251
334
  if (!matchesEventFilter(definition.event?.match, summary))
@@ -253,6 +336,8 @@ export class GithubPollSource {
253
336
  await this.fireSafely(onFire, match, summary);
254
337
  }
255
338
  }
339
+ heads = boundHeads(heads, response.items);
340
+ await this.store.setTriggerAdapterState(adapterId, "heads", JSON.stringify(heads));
256
341
  await this.store.setTriggerAdapterState(adapterId, "cursor", JSON.stringify({ updatedAt: newest.updated_at, number: newest.number }));
257
342
  }
258
343
  async fireSafely(onFire, match, summary) {
@@ -0,0 +1,8 @@
1
+ import type { ResolvePriorEnvelopeResult } from "./envelopeRouting.js";
2
+ import type { StageArtifactGrant } from "./stageRoots.js";
3
+ export { isGrantableArtifactPath } from "./artifactPath.js";
4
+ /** Sandbox path for one artifact. Colliding names from different attempts include the attempt. */
5
+ export declare function assignGrantContainerPaths(listedPaths: readonly string[]): Map<string, string>;
6
+ export declare function artifactGrantsFromPriors(workspaceDir: string, prior: Extract<ResolvePriorEnvelopeResult, {
7
+ ok: true;
8
+ }>): Promise<StageArtifactGrant[]>;
@@ -0,0 +1,105 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { isInsideDir } from "../runstore/workspaceLayout.js";
4
+ import { acceptArtifactListedPath, isGrantableArtifactPath, } from "./artifactPath.js";
5
+ import { inspectArtifactUnderRoot } from "./artifactDiskInspect.js";
6
+ import { SANDBOX_MOUNT_PATH } from "./sandboxContainer.js";
7
+ export { isGrantableArtifactPath } from "./artifactPath.js";
8
+ function successArtifactsFromPrior(prior) {
9
+ const envelopes = [];
10
+ if (prior.prior !== null && prior.prior !== undefined) {
11
+ envelopes.push(prior.prior);
12
+ }
13
+ if (prior.joinPriors !== undefined) {
14
+ envelopes.push(...prior.joinPriors);
15
+ }
16
+ if (prior.priorEnvelopesByStage !== undefined) {
17
+ for (const value of Object.values(prior.priorEnvelopesByStage)) {
18
+ if (Array.isArray(value))
19
+ envelopes.push(...value);
20
+ else
21
+ envelopes.push(value);
22
+ }
23
+ }
24
+ const listed = [];
25
+ for (const envelope of envelopes) {
26
+ if (envelope.status !== "success")
27
+ continue;
28
+ listed.push(...envelope.artifacts);
29
+ }
30
+ return listed;
31
+ }
32
+ /**
33
+ * Soft-skip grant host resolution: missing / symlink / non-file → null.
34
+ * Closed-posture walk via inspect; realpath must stay inside the workspace.
35
+ */
36
+ async function resolveGrantableHostFile(workspaceRoot, listedPath) {
37
+ try {
38
+ const presence = await inspectArtifactUnderRoot(workspaceRoot, listedPath);
39
+ if (presence.file_type !== "file" || presence.absolutePath === undefined) {
40
+ return null;
41
+ }
42
+ const current = presence.absolutePath;
43
+ if (!isInsideDir(current, workspaceRoot) || current === workspaceRoot) {
44
+ return null;
45
+ }
46
+ const workspaceReal = await realpath(workspaceRoot);
47
+ const fileReal = await realpath(current);
48
+ if (!isInsideDir(fileReal, workspaceReal) || fileReal === workspaceReal) {
49
+ return null;
50
+ }
51
+ return current;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /** Sandbox path for one artifact. Colliding names from different attempts include the attempt. */
58
+ export function assignGrantContainerPaths(listedPaths) {
59
+ const parsed = listedPaths.map((listedPath) => {
60
+ const parts = acceptArtifactListedPath(listedPath, { kind: "grantable" });
61
+ const base = parts === undefined
62
+ ? null
63
+ : `${SANDBOX_MOUNT_PATH}/grants/${parts.stageId}/${parts.relative}`;
64
+ return { listedPath, parts, base };
65
+ });
66
+ const counts = new Map();
67
+ for (const item of parsed) {
68
+ if (item.base === null)
69
+ continue;
70
+ counts.set(item.base, (counts.get(item.base) ?? 0) + 1);
71
+ }
72
+ const assigned = new Map();
73
+ for (const item of parsed) {
74
+ if (item.parts === undefined || item.base === null)
75
+ continue;
76
+ const containerPath = (counts.get(item.base) ?? 0) > 1
77
+ ? `${SANDBOX_MOUNT_PATH}/grants/${item.parts.stageId}/attempt-${item.parts.attempt}/${item.parts.relative}`
78
+ : item.base;
79
+ assigned.set(item.listedPath, containerPath);
80
+ }
81
+ return assigned;
82
+ }
83
+ export async function artifactGrantsFromPriors(workspaceDir, prior) {
84
+ const workspaceRoot = path.resolve(workspaceDir);
85
+ const listed = successArtifactsFromPrior(prior).filter(isGrantableArtifactPath);
86
+ const containerPaths = assignGrantContainerPaths(listed);
87
+ const grants = [];
88
+ const seen = new Set();
89
+ for (const listedPath of listed) {
90
+ const containerPath = containerPaths.get(listedPath);
91
+ if (containerPath === undefined)
92
+ continue;
93
+ const hostPath = await resolveGrantableHostFile(workspaceRoot, listedPath);
94
+ if (hostPath === null || seen.has(hostPath))
95
+ continue;
96
+ seen.add(hostPath);
97
+ grants.push({
98
+ listedPath,
99
+ hostPath,
100
+ containerPath,
101
+ kind: "file",
102
+ });
103
+ }
104
+ return grants;
105
+ }
@@ -0,0 +1,46 @@
1
+ import { type RunDetail, type RunPipelineDagSnapshot, type RunStatus, type StageSnapshot } from "../runstore/port.js";
2
+ export type StageRetrySubject = Pick<StageSnapshot, "stage_id" | "status" | "definition_id" | "envelope">;
3
+ export type RunActionEligibilityInput = {
4
+ runStatus: RunStatus;
5
+ stage?: StageRetrySubject;
6
+ stages?: ReadonlyArray<Pick<StageSnapshot, "status" | "stage_id">>;
7
+ recoveryActive?: boolean;
8
+ dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
9
+ };
10
+ export type RunActionEligibility = {
11
+ canRetry: boolean;
12
+ canCancel: boolean;
13
+ canAbandon: boolean;
14
+ blockers: string[];
15
+ };
16
+ export declare function resumesRunOnStageRetry(status: RunStatus): boolean;
17
+ export declare function canCancelRun(status: RunStatus): boolean;
18
+ export declare function canAbandonStage(stageStatus: string): boolean;
19
+ export declare function succeededStageRetryBlocker(stageSnap: StageRetrySubject): string | undefined;
20
+ export declare function stageRetryBlocker(input: {
21
+ runStatus: RunStatus;
22
+ stage: StageRetrySubject;
23
+ stages?: ReadonlyArray<Pick<StageSnapshot, "status" | "stage_id">>;
24
+ recoveryActive?: boolean;
25
+ dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
26
+ }): string | undefined;
27
+ export declare function canRetryStage(input: {
28
+ runStatus: RunStatus;
29
+ stage: StageRetrySubject;
30
+ stages?: ReadonlyArray<Pick<StageSnapshot, "status" | "stage_id">>;
31
+ recoveryActive?: boolean;
32
+ dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
33
+ }): boolean;
34
+ export declare function runActionEligibility(input: RunActionEligibilityInput): RunActionEligibility;
35
+ export declare function assertStageRetryEligible(detail: RunDetail, stageId: string, opts?: {
36
+ recoveryActive?: boolean;
37
+ persistedStatus?: RunStatus;
38
+ dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
39
+ }): {
40
+ ok: true;
41
+ } | {
42
+ ok: false;
43
+ reason: string;
44
+ status: number;
45
+ code: string;
46
+ };
@@ -0,0 +1,123 @@
1
+ import { findUnhandledFailedStage, } from "../runstore/port.js";
2
+ import { isRunClockTerminal } from "../runstore/runStatusClock.js";
3
+ export function resumesRunOnStageRetry(status) {
4
+ return isRunClockTerminal(status);
5
+ }
6
+ export function canCancelRun(status) {
7
+ return status !== "succeeded" && status !== "failed";
8
+ }
9
+ export function canAbandonStage(stageStatus) {
10
+ return stageStatus === "running";
11
+ }
12
+ export function succeededStageRetryBlocker(stageSnap) {
13
+ if (stageSnap.definition_id !== undefined &&
14
+ stageSnap.definition_id !== stageSnap.stage_id) {
15
+ return `Cannot re-run a succeeded clone instance stage (stage=${stageSnap.stage_id}, definition=${stageSnap.definition_id}): clone instances are created by upstream fan-out and cannot be rebuilt in place. Use rerun to start a new run.`;
16
+ }
17
+ if (stageSnap.envelope?.fork_choice !== undefined) {
18
+ return `Cannot re-run a succeeded fork stage (stage=${stageSnap.stage_id}): its envelope recorded a fork_choice and skipped branches cannot be restored. Use rerun to start a new run.`;
19
+ }
20
+ return undefined;
21
+ }
22
+ export function stageRetryBlocker(input) {
23
+ const { stage, runStatus } = input;
24
+ if (stage.status === "waiting_for_input") {
25
+ return `Stage is waiting for input and cannot be retried`;
26
+ }
27
+ const recoveryActive = input.recoveryActive === true;
28
+ if (!resumesRunOnStageRetry(runStatus)) {
29
+ const stages = input.stages ?? [];
30
+ const acceptedPendingRetry = runStatus === "running" &&
31
+ stage.status === "failed" &&
32
+ findUnhandledFailedStage(stages, input.dag) === undefined &&
33
+ stages.some((s) => s.status === "pending");
34
+ if (!(runStatus === "running" && recoveryActive) && !acceptedPendingRetry) {
35
+ return `Run is not failed or succeeded (status=${runStatus})`;
36
+ }
37
+ }
38
+ if (stage.status !== "failed" && stage.status !== "succeeded") {
39
+ return `Stage is not failed or succeeded (status=${stage.status})`;
40
+ }
41
+ if (stage.status === "succeeded") {
42
+ return succeededStageRetryBlocker(stage);
43
+ }
44
+ return undefined;
45
+ }
46
+ export function canRetryStage(input) {
47
+ return stageRetryBlocker(input) === undefined;
48
+ }
49
+ export function runActionEligibility(input) {
50
+ const blockers = [];
51
+ const canCancel = canCancelRun(input.runStatus);
52
+ if (!canCancel) {
53
+ blockers.push(`Run is ${input.runStatus} and cannot be cancelled`);
54
+ }
55
+ const canAbandon = input.stage !== undefined && canAbandonStage(input.stage.status);
56
+ if (input.stage !== undefined && !canAbandon) {
57
+ blockers.push(`Stage is not running (status=${input.stage.status})`);
58
+ }
59
+ let canRetry = false;
60
+ if (input.stage === undefined) {
61
+ blockers.push("Stage is required to retry");
62
+ }
63
+ else {
64
+ const retryBlocker = stageRetryBlocker({
65
+ runStatus: input.runStatus,
66
+ stage: input.stage,
67
+ stages: input.stages,
68
+ recoveryActive: input.recoveryActive,
69
+ dag: input.dag,
70
+ });
71
+ if (retryBlocker === undefined) {
72
+ canRetry = true;
73
+ }
74
+ else {
75
+ blockers.push(retryBlocker);
76
+ }
77
+ }
78
+ return { canRetry, canCancel, canAbandon, blockers };
79
+ }
80
+ export function assertStageRetryEligible(detail, stageId, opts) {
81
+ const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
82
+ if (!stageSnap) {
83
+ return {
84
+ ok: false,
85
+ reason: `Stage not found: ${stageId}`,
86
+ status: 404,
87
+ code: "stage_not_found",
88
+ };
89
+ }
90
+ const runStatus = opts?.persistedStatus ?? detail.status;
91
+ const blocker = stageRetryBlocker({
92
+ runStatus,
93
+ stage: stageSnap,
94
+ stages: detail.stages,
95
+ recoveryActive: opts?.recoveryActive,
96
+ dag: opts?.dag,
97
+ });
98
+ if (blocker === undefined) {
99
+ return { ok: true };
100
+ }
101
+ if (blocker.includes("waiting for input")) {
102
+ return {
103
+ ok: false,
104
+ reason: blocker,
105
+ status: 409,
106
+ code: "hitl_not_retriable",
107
+ };
108
+ }
109
+ if (blocker.startsWith("Stage is not failed or succeeded")) {
110
+ return {
111
+ ok: false,
112
+ reason: blocker,
113
+ status: 409,
114
+ code: "stage_not_failed",
115
+ };
116
+ }
117
+ return {
118
+ ok: false,
119
+ reason: blocker,
120
+ status: 409,
121
+ code: "run_not_retryable",
122
+ };
123
+ }