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.
- package/README.md +12 -3
- package/dist/agent/activity.d.ts +18 -0
- package/dist/agent/claudeAdapter.js +7 -0
- package/dist/agent/claudeTools.d.ts +27 -18
- package/dist/agent/claudeTools.js +54 -15
- package/dist/agent/compactionEvents.d.ts +34 -0
- package/dist/agent/compactionEvents.js +37 -0
- package/dist/agent/openRouterRouting.d.ts +7 -0
- package/dist/agent/openRouterRouting.js +23 -0
- package/dist/agent/piAdapter.d.ts +6 -0
- package/dist/agent/piAdapter.js +128 -27
- package/dist/agent/piSettings.d.ts +7 -0
- package/dist/agent/piSettings.js +9 -0
- package/dist/agent/sandboxPiTools.d.ts +3 -1
- package/dist/agent/sandboxPiTools.js +8 -2
- package/dist/agent/stageTurnDriver.d.ts +11 -2
- package/dist/agent/stageTurnDriver.js +45 -4
- package/dist/agent/toolOutputBudget.d.ts +15 -1
- package/dist/agent/toolOutputBudget.js +18 -0
- package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
- package/dist/agent/toolOutputBudgetExtension.js +40 -3
- package/dist/agents/authorChat.d.ts +1 -0
- package/dist/agents/authorChat.js +7 -0
- package/dist/agents/authorSessionStore.d.ts +3 -0
- package/dist/agents/authorSessionStore.js +17 -2
- package/dist/agents/evalGate.js +1 -0
- package/dist/agents/hostContext.d.ts +1 -1
- package/dist/agents/runtime/piRuntime.d.ts +5 -1
- package/dist/agents/runtime/piRuntime.js +46 -12
- package/dist/agents/service.js +3 -2
- package/dist/agents/sessions.js +3 -1
- package/dist/cli/evalCommand.d.ts +1 -1
- package/dist/cli/evalCommand.js +39 -2
- package/dist/cli/hostClient.d.ts +6 -0
- package/dist/cli/hostClient.js +4 -0
- package/dist/compaction/directives.d.ts +18 -0
- package/dist/compaction/directives.js +119 -0
- package/dist/compaction/extension.d.ts +20 -0
- package/dist/compaction/extension.js +78 -0
- package/dist/compaction/mask.d.ts +18 -0
- package/dist/compaction/mask.js +155 -0
- package/dist/compaction/pin.d.ts +21 -0
- package/dist/compaction/pin.js +270 -0
- package/dist/compaction/scrub.d.ts +7 -0
- package/dist/compaction/scrub.js +26 -0
- package/dist/compaction/sessionWiring.d.ts +22 -0
- package/dist/compaction/sessionWiring.js +26 -0
- package/dist/compaction/summarize.d.ts +79 -0
- package/dist/compaction/summarize.js +171 -0
- package/dist/compaction/toolPolicy.d.ts +3 -0
- package/dist/compaction/toolPolicy.js +33 -0
- package/dist/compaction/types.d.ts +45 -0
- package/dist/compaction/types.js +7 -0
- package/dist/compaction/workingState.d.ts +56 -0
- package/dist/compaction/workingState.js +198 -0
- package/dist/config/createPipeline.js +2 -0
- package/dist/config/createTrigger.js +7 -1
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
- package/dist/config/ensureCatalogScanRoot.js +6 -0
- package/dist/config/evalCatalog.js +30 -2
- package/dist/config/githubTriggerAction.d.ts +8 -0
- package/dist/config/githubTriggerAction.js +29 -0
- package/dist/config/hostConfig.js +2 -0
- package/dist/config/loadEval.d.ts +3 -1
- package/dist/config/loadEval.js +96 -10
- package/dist/config/pipelineNeeds.d.ts +2 -0
- package/dist/config/pipelineNeeds.js +16 -3
- package/dist/config/pipelineRoute.js +5 -4
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +12 -0
- package/dist/config/validateEval.js +60 -3
- package/dist/config/writeEval.js +2 -2
- package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
- package/dist/envelope/assertEmittedArtifacts.js +43 -0
- package/dist/envelope/preEmitChecks.d.ts +3 -0
- package/dist/evals/calibration.d.ts +21 -0
- package/dist/evals/calibration.js +69 -0
- package/dist/evals/calibrationSuggest.d.ts +29 -0
- package/dist/evals/calibrationSuggest.js +72 -0
- package/dist/evals/calibrator.d.ts +46 -0
- package/dist/evals/calibrator.js +148 -0
- package/dist/evals/compare.d.ts +1 -1
- package/dist/evals/compare.js +3 -0
- package/dist/evals/constants.d.ts +7 -3
- package/dist/evals/constants.js +7 -1
- package/dist/evals/engine/FakeEngine.d.ts +2 -1
- package/dist/evals/engine/FakeEngine.js +9 -3
- package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +4 -2
- package/dist/evals/engine/promptfoo/generate.d.ts +3 -1
- package/dist/evals/engine/promptfoo/generate.js +43 -23
- package/dist/evals/engine/promptfoo/judgeProviders.js +2 -1
- package/dist/evals/engine/promptfoo/normalize.d.ts +4 -1
- package/dist/evals/engine/promptfoo/normalize.js +16 -3
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +3 -1
- package/dist/evals/engine/promptfoo/rubricPrompt.js +15 -5
- package/dist/evals/errors.d.ts +7 -0
- package/dist/evals/errors.js +10 -0
- package/dist/evals/judgeProviders.d.ts +6 -0
- package/dist/evals/judgeProviders.js +14 -0
- package/dist/evals/judged.d.ts +7 -0
- package/dist/evals/judged.js +7 -0
- package/dist/evals/manager.d.ts +27 -8
- package/dist/evals/manager.js +73 -12
- package/dist/evals/plan.d.ts +10 -1
- package/dist/evals/plan.js +33 -13
- package/dist/evals/planInternalAgent.d.ts +19 -0
- package/dist/evals/planInternalAgent.js +201 -0
- package/dist/evals/read/api.d.ts +18 -0
- package/dist/evals/read/api.js +10 -1
- package/dist/evals/read/labelQueue.d.ts +45 -0
- package/dist/evals/read/labelQueue.js +83 -0
- package/dist/evals/read/list.js +10 -1
- package/dist/evals/read/runs.d.ts +5 -0
- package/dist/evals/read/runs.js +7 -1
- package/dist/evals/runner/caseRunner.d.ts +2 -1
- package/dist/evals/runner/caseRunner.js +16 -5
- package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
- package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
- package/dist/evals/snapshot.d.ts +10 -2
- package/dist/evals/snapshot.js +8 -3
- package/dist/evals/stats.d.ts +40 -0
- package/dist/evals/stats.js +69 -0
- package/dist/evals/storeTypes.d.ts +42 -0
- package/dist/evals/types.d.ts +25 -2
- package/dist/evals/types.js +4 -1
- package/dist/evals/votes.d.ts +10 -0
- package/dist/evals/votes.js +60 -0
- package/dist/guide/cardContract.d.ts +133 -0
- package/dist/guide/cardContract.js +156 -0
- package/dist/guide/chat.d.ts +45 -0
- package/dist/guide/chat.js +378 -0
- package/dist/guide/knowledge/index.d.ts +14 -0
- package/dist/guide/knowledge/index.js +109 -0
- package/dist/guide/knowledge/topics/agents.d.ts +2 -0
- package/dist/guide/knowledge/topics/agents.js +58 -0
- package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
- package/dist/guide/knowledge/topics/bestPractices.js +67 -0
- package/dist/guide/knowledge/topics/connections.d.ts +2 -0
- package/dist/guide/knowledge/topics/connections.js +66 -0
- package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
- package/dist/guide/knowledge/topics/consoleMap.js +55 -0
- package/dist/guide/knowledge/topics/evals.d.ts +2 -0
- package/dist/guide/knowledge/topics/evals.js +72 -0
- package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
- package/dist/guide/knowledge/topics/limitations.js +84 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
- package/dist/guide/knowledge/topics/memories.d.ts +2 -0
- package/dist/guide/knowledge/topics/memories.js +63 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
- package/dist/guide/knowledge/topics/overview.d.ts +2 -0
- package/dist/guide/knowledge/topics/overview.js +56 -0
- package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
- package/dist/guide/knowledge/topics/pipelines.js +93 -0
- package/dist/guide/knowledge/topics/stages.d.ts +2 -0
- package/dist/guide/knowledge/topics/stages.js +54 -0
- package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
- package/dist/guide/knowledge/topics/tasks.js +52 -0
- package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
- package/dist/guide/knowledge/topics/triggers.js +68 -0
- package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
- package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
- package/dist/guide/knowledgeTools.d.ts +2 -0
- package/dist/guide/knowledgeTools.js +56 -0
- package/dist/guide/showUi.d.ts +6 -0
- package/dist/guide/showUi.js +546 -0
- package/dist/guide/threadStore.d.ts +48 -0
- package/dist/guide/threadStore.js +189 -0
- package/dist/guide/writeRoot.d.ts +11 -0
- package/dist/guide/writeRoot.js +70 -0
- package/dist/hosted/translate.js +2 -0
- package/dist/mcp/agentTools.js +12 -0
- package/dist/mcp/catalogTools.js +18 -20
- package/dist/mcp/catalogWriteTools.d.ts +8 -0
- package/dist/mcp/catalogWriteTools.js +225 -0
- package/dist/mcp/connectionTools.d.ts +3 -0
- package/dist/mcp/connectionTools.js +85 -0
- package/dist/mcp/controlTools.js +34 -1
- package/dist/mcp/deps.d.ts +9 -0
- package/dist/mcp/evalTools.js +723 -21
- package/dist/mcp/memoryHostTools.d.ts +3 -0
- package/dist/mcp/memoryHostTools.js +72 -0
- package/dist/mcp/pipelineAuthorTools.js +14 -1
- package/dist/mcp/server.js +5 -0
- package/dist/mcp/tools.js +6 -0
- package/dist/mcp/triggerTools.js +77 -31
- package/dist/operatorAgent/evalGate.d.ts +22 -0
- package/dist/operatorAgent/evalGate.js +77 -0
- package/dist/operatorAgent/fakeHost.js +10 -2
- package/dist/operatorAgent/host.js +18 -2
- package/dist/operatorAgent/index.d.ts +1 -0
- package/dist/operatorAgent/index.js +1 -0
- package/dist/operatorAgent/internalAgents.d.ts +11 -0
- package/dist/operatorAgent/internalAgents.js +13 -0
- package/dist/operatorAgent/piModel.d.ts +5 -1
- package/dist/operatorAgent/piModel.js +15 -10
- package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
- package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
- package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
- package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
- package/dist/operatorAgent/profiles/guideTools.js +123 -0
- package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
- package/dist/operatorAgent/profiles/stageflowGuide.js +133 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
- package/dist/operatorAgent/types.d.ts +23 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/port.d.ts +13 -1
- package/dist/runstore/retention.d.ts +2 -1
- package/dist/runstore/retention.js +3 -7
- package/dist/runstore/runStatusClock.d.ts +17 -0
- package/dist/runstore/runStatusClock.js +19 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +70 -45
- package/dist/runstore/sqlite/calibrationStore.d.ts +10 -0
- package/dist/runstore/sqlite/calibrationStore.js +106 -0
- package/dist/runstore/sqlite/evalStore.d.ts +5 -0
- package/dist/runstore/sqlite/evalStore.js +7 -0
- package/dist/runstore/sqlite/migrations/018-eval-grader-labels.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/018-eval-grader-labels.js +43 -0
- package/dist/runstore/sqlite/migrations/index.js +2 -0
- package/dist/runstore/workspaceLayout.js +6 -1
- package/dist/runtime/artifactDiskInspect.d.ts +14 -0
- package/dist/runtime/artifactDiskInspect.js +52 -0
- package/dist/runtime/artifactPath.d.ts +24 -0
- package/dist/runtime/artifactPath.js +84 -0
- package/dist/runtime/completionCheckRunner.js +15 -43
- package/dist/runtime/githubPollSource.d.ts +22 -4
- package/dist/runtime/githubPollSource.js +93 -8
- package/dist/runtime/priorArtifactGrants.d.ts +8 -0
- package/dist/runtime/priorArtifactGrants.js +105 -0
- package/dist/runtime/runActionEligibility.d.ts +46 -0
- package/dist/runtime/runActionEligibility.js +123 -0
- package/dist/runtime/runManager.d.ts +4 -1
- package/dist/runtime/runManager.js +52 -10
- package/dist/runtime/runRetryCoordinator.d.ts +2 -14
- package/dist/runtime/runRetryCoordinator.js +3 -67
- package/dist/runtime/sandboxContainer.d.ts +7 -0
- package/dist/runtime/sandboxContainer.js +48 -6
- package/dist/runtime/sandboxDesk.d.ts +2 -1
- package/dist/runtime/sandboxDesk.js +11 -12
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageRoots.d.ts +12 -0
- package/dist/runtime/stageSandbox.d.ts +2 -1
- package/dist/runtime/stageSandbox.js +18 -2
- package/dist/runtime/stageWorker.js +20 -0
- package/dist/server/agentAuthorRoutes.js +12 -3
- package/dist/server/bootstrap.d.ts +5 -0
- package/dist/server/bootstrap.js +14 -1
- package/dist/server/evalRoutes.js +251 -12
- package/dist/server/guideRoutes.d.ts +5 -0
- package/dist/server/guideRoutes.js +166 -0
- package/dist/server/http.d.ts +10 -0
- package/dist/server/http.js +35 -1
- package/dist/server/internalAgentRoutes.d.ts +6 -0
- package/dist/server/internalAgentRoutes.js +12 -0
- package/dist/server/mcpHost.js +2 -0
- package/dist/toolkit/meta.d.ts +1 -1
- package/dist/toolkit/meta.js +41 -1
- package/dist/toolkit/profiles.d.ts +5 -0
- package/dist/toolkit/profiles.js +12 -0
- package/dist/tools/askOperator.js +2 -1
- package/dist/tools/emitStageEnvelope.d.ts +17 -16
- package/dist/tools/emitStageEnvelope.js +46 -32
- package/dist/tools/stageToolNames.d.ts +4 -0
- package/dist/tools/stageToolNames.js +4 -0
- package/dist/tools/workingState.d.ts +40 -0
- package/dist/tools/workingState.js +108 -0
- package/dist/tools/writeStageArtifact.d.ts +0 -9
- package/dist/tools/writeStageArtifact.js +2 -1
- package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-BS2BBbCx.js} +2 -2
- package/dist/ui/assets/adapters-DPD-cAsy.js +17 -0
- package/dist/ui/assets/chat-BD1-VTT6.js +2 -0
- package/dist/ui/assets/chat-DXnQv9st.css +2 -0
- package/dist/ui/assets/main-AX5XNLtX.js +278 -0
- package/dist/ui/assets/{main-CHlmTNJe.css → main-CFX0_n8J.css} +1 -1
- package/dist/ui/chat.html +3 -4
- package/dist/ui/index.html +3 -4
- package/dist/workshop/chatTurn.js +1 -1
- package/dist/workshop/workshopPin.d.ts +3 -3
- package/dist/workshop/workshopPin.js +16 -15
- package/examples/compaction-stress/README.md +30 -0
- package/examples/compaction-stress/check-run.mjs +53 -0
- package/examples/compaction-stress/collect-evidence.yaml +60 -0
- package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
- package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
- package/examples/compaction-stress/generate-evidence.mjs +110 -0
- package/examples/compaction-stress/root-cause.yaml +26 -0
- package/examples/compaction-stress/stageflow.yaml +9 -0
- package/examples/compaction-stress/write-postmortem.yaml +40 -0
- package/examples/evals/README.md +18 -0
- package/examples/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/evals/guide-behavior.eval.yaml +35 -0
- package/examples/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/evals/guide-depth.eval.yaml +169 -0
- package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
- package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/memory/evals/guide-depth.eval.yaml +169 -0
- package/package.json +1 -1
- package/skills/stageflow/references/control-surface.md +1 -1
- package/skills/stageflow/references/evals.md +1 -0
- package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
- package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
- package/dist/ui/assets/chat-Df_wCpak.js +0 -5
- package/dist/ui/assets/chat-P7KQznHm.css +0 -2
- 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 {
|
|
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
|
|
167
|
-
|
|
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
|
-
|
|
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:
|
|
206
|
-
size_bytes: stats.size,
|
|
207
|
-
sha256: createHash("sha256").update(content).digest("hex"),
|
|
176
|
+
file_type: presence.file_type,
|
|
208
177
|
};
|
|
209
178
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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" | "
|
|
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
|
|
63
|
-
* never fires for a repo's pre-existing PR history,
|
|
64
|
-
* `ScheduleSource` seeds `next_run_at` without firing on
|
|
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
|
-
|
|
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:
|
|
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
|
|
115
|
-
* never fires for a repo's pre-existing PR history,
|
|
116
|
-
* `ScheduleSource` seeds `next_run_at` without firing on
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|