stageflow 0.42.0 → 0.44.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 (275) 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/hostContext.d.ts +1 -1
  27. package/dist/agents/runtime/piRuntime.d.ts +5 -1
  28. package/dist/agents/runtime/piRuntime.js +46 -12
  29. package/dist/agents/service.js +3 -2
  30. package/dist/agents/sessions.js +3 -1
  31. package/dist/compaction/directives.d.ts +18 -0
  32. package/dist/compaction/directives.js +119 -0
  33. package/dist/compaction/extension.d.ts +20 -0
  34. package/dist/compaction/extension.js +78 -0
  35. package/dist/compaction/mask.d.ts +18 -0
  36. package/dist/compaction/mask.js +155 -0
  37. package/dist/compaction/pin.d.ts +21 -0
  38. package/dist/compaction/pin.js +270 -0
  39. package/dist/compaction/scrub.d.ts +7 -0
  40. package/dist/compaction/scrub.js +26 -0
  41. package/dist/compaction/sessionWiring.d.ts +22 -0
  42. package/dist/compaction/sessionWiring.js +26 -0
  43. package/dist/compaction/summarize.d.ts +79 -0
  44. package/dist/compaction/summarize.js +171 -0
  45. package/dist/compaction/toolPolicy.d.ts +3 -0
  46. package/dist/compaction/toolPolicy.js +33 -0
  47. package/dist/compaction/types.d.ts +45 -0
  48. package/dist/compaction/types.js +7 -0
  49. package/dist/compaction/workingState.d.ts +56 -0
  50. package/dist/compaction/workingState.js +198 -0
  51. package/dist/config/createPipeline.js +2 -0
  52. package/dist/config/createTrigger.js +7 -1
  53. package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
  54. package/dist/config/ensureCatalogScanRoot.js +6 -0
  55. package/dist/config/evalCatalog.js +30 -2
  56. package/dist/config/githubTriggerAction.d.ts +8 -0
  57. package/dist/config/githubTriggerAction.js +29 -0
  58. package/dist/config/hostConfig.js +2 -0
  59. package/dist/config/loadEval.js +21 -3
  60. package/dist/config/pipelineNeeds.d.ts +2 -0
  61. package/dist/config/pipelineNeeds.js +16 -3
  62. package/dist/config/pipelineRoute.js +5 -4
  63. package/dist/config/validateCatalog.d.ts +1 -1
  64. package/dist/config/validateCatalog.js +12 -0
  65. package/dist/config/validateEval.js +43 -2
  66. package/dist/config/writeEval.js +2 -2
  67. package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
  68. package/dist/envelope/assertEmittedArtifacts.js +43 -0
  69. package/dist/envelope/preEmitChecks.d.ts +3 -0
  70. package/dist/evals/constants.d.ts +3 -1
  71. package/dist/evals/constants.js +3 -1
  72. package/dist/evals/engine/FakeEngine.d.ts +1 -1
  73. package/dist/evals/engine/FakeEngine.js +2 -1
  74. package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
  75. package/dist/evals/engine/promptfoo/PromptfooEngine.js +2 -1
  76. package/dist/evals/engine/promptfoo/generate.d.ts +1 -1
  77. package/dist/evals/engine/promptfoo/generate.js +11 -8
  78. package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +1 -1
  79. package/dist/evals/engine/promptfoo/rubricPrompt.js +5 -3
  80. package/dist/evals/manager.d.ts +13 -0
  81. package/dist/evals/manager.js +10 -2
  82. package/dist/evals/plan.d.ts +8 -1
  83. package/dist/evals/plan.js +28 -9
  84. package/dist/evals/planInternalAgent.d.ts +19 -0
  85. package/dist/evals/planInternalAgent.js +205 -0
  86. package/dist/evals/read/api.d.ts +12 -0
  87. package/dist/evals/read/api.js +5 -0
  88. package/dist/evals/read/list.js +10 -1
  89. package/dist/evals/read/runs.d.ts +5 -0
  90. package/dist/evals/read/runs.js +7 -1
  91. package/dist/evals/runner/caseRunner.d.ts +2 -1
  92. package/dist/evals/runner/caseRunner.js +16 -5
  93. package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
  94. package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
  95. package/dist/evals/snapshot.d.ts +10 -2
  96. package/dist/evals/snapshot.js +8 -3
  97. package/dist/evals/types.d.ts +12 -2
  98. package/dist/evals/types.js +4 -1
  99. package/dist/guide/cardContract.d.ts +133 -0
  100. package/dist/guide/cardContract.js +156 -0
  101. package/dist/guide/chat.d.ts +45 -0
  102. package/dist/guide/chat.js +378 -0
  103. package/dist/guide/knowledge/index.d.ts +14 -0
  104. package/dist/guide/knowledge/index.js +109 -0
  105. package/dist/guide/knowledge/topics/agents.d.ts +2 -0
  106. package/dist/guide/knowledge/topics/agents.js +58 -0
  107. package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
  108. package/dist/guide/knowledge/topics/bestPractices.js +67 -0
  109. package/dist/guide/knowledge/topics/connections.d.ts +2 -0
  110. package/dist/guide/knowledge/topics/connections.js +66 -0
  111. package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
  112. package/dist/guide/knowledge/topics/consoleMap.js +55 -0
  113. package/dist/guide/knowledge/topics/evals.d.ts +2 -0
  114. package/dist/guide/knowledge/topics/evals.js +72 -0
  115. package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
  116. package/dist/guide/knowledge/topics/limitations.js +84 -0
  117. package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
  118. package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
  119. package/dist/guide/knowledge/topics/memories.d.ts +2 -0
  120. package/dist/guide/knowledge/topics/memories.js +63 -0
  121. package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
  122. package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
  123. package/dist/guide/knowledge/topics/overview.d.ts +2 -0
  124. package/dist/guide/knowledge/topics/overview.js +56 -0
  125. package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
  126. package/dist/guide/knowledge/topics/pipelines.js +93 -0
  127. package/dist/guide/knowledge/topics/stages.d.ts +2 -0
  128. package/dist/guide/knowledge/topics/stages.js +54 -0
  129. package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
  130. package/dist/guide/knowledge/topics/tasks.js +52 -0
  131. package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
  132. package/dist/guide/knowledge/topics/triggers.js +68 -0
  133. package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
  134. package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
  135. package/dist/guide/knowledgeTools.d.ts +2 -0
  136. package/dist/guide/knowledgeTools.js +56 -0
  137. package/dist/guide/showUi.d.ts +6 -0
  138. package/dist/guide/showUi.js +546 -0
  139. package/dist/guide/threadStore.d.ts +48 -0
  140. package/dist/guide/threadStore.js +189 -0
  141. package/dist/guide/writeRoot.d.ts +11 -0
  142. package/dist/guide/writeRoot.js +70 -0
  143. package/dist/hosted/translate.js +2 -0
  144. package/dist/mcp/agentTools.js +12 -0
  145. package/dist/mcp/catalogTools.js +18 -20
  146. package/dist/mcp/catalogWriteTools.d.ts +8 -0
  147. package/dist/mcp/catalogWriteTools.js +225 -0
  148. package/dist/mcp/connectionTools.d.ts +3 -0
  149. package/dist/mcp/connectionTools.js +85 -0
  150. package/dist/mcp/controlTools.js +34 -1
  151. package/dist/mcp/deps.d.ts +9 -0
  152. package/dist/mcp/evalTools.js +684 -21
  153. package/dist/mcp/memoryHostTools.d.ts +3 -0
  154. package/dist/mcp/memoryHostTools.js +72 -0
  155. package/dist/mcp/pipelineAuthorTools.js +14 -1
  156. package/dist/mcp/server.js +5 -0
  157. package/dist/mcp/tools.js +6 -0
  158. package/dist/mcp/triggerTools.js +77 -31
  159. package/dist/operatorAgent/evalGate.d.ts +22 -0
  160. package/dist/operatorAgent/evalGate.js +77 -0
  161. package/dist/operatorAgent/fakeHost.js +10 -2
  162. package/dist/operatorAgent/host.js +18 -2
  163. package/dist/operatorAgent/index.d.ts +1 -0
  164. package/dist/operatorAgent/index.js +1 -0
  165. package/dist/operatorAgent/internalAgents.d.ts +11 -0
  166. package/dist/operatorAgent/internalAgents.js +13 -0
  167. package/dist/operatorAgent/piModel.d.ts +5 -1
  168. package/dist/operatorAgent/piModel.js +15 -10
  169. package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
  170. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
  171. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
  172. package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
  173. package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
  174. package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
  175. package/dist/operatorAgent/profiles/guideTools.js +123 -0
  176. package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
  177. package/dist/operatorAgent/profiles/stageflowGuide.js +132 -0
  178. package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
  179. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  180. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
  181. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
  182. package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
  183. package/dist/operatorAgent/types.d.ts +23 -0
  184. package/dist/package-meta.d.ts +1 -1
  185. package/dist/package-meta.js +1 -1
  186. package/dist/runstore/retention.d.ts +2 -1
  187. package/dist/runstore/retention.js +3 -7
  188. package/dist/runstore/runStatusClock.d.ts +17 -0
  189. package/dist/runstore/runStatusClock.js +19 -0
  190. package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
  191. package/dist/runstore/sqlite/SqliteRunStore.js +45 -45
  192. package/dist/runstore/workspaceLayout.js +6 -1
  193. package/dist/runtime/artifactDiskInspect.d.ts +14 -0
  194. package/dist/runtime/artifactDiskInspect.js +52 -0
  195. package/dist/runtime/artifactPath.d.ts +24 -0
  196. package/dist/runtime/artifactPath.js +84 -0
  197. package/dist/runtime/completionCheckRunner.js +15 -43
  198. package/dist/runtime/githubPollSource.d.ts +22 -4
  199. package/dist/runtime/githubPollSource.js +93 -8
  200. package/dist/runtime/priorArtifactGrants.d.ts +8 -0
  201. package/dist/runtime/priorArtifactGrants.js +105 -0
  202. package/dist/runtime/runActionEligibility.d.ts +46 -0
  203. package/dist/runtime/runActionEligibility.js +123 -0
  204. package/dist/runtime/runManager.d.ts +4 -1
  205. package/dist/runtime/runManager.js +52 -10
  206. package/dist/runtime/runRetryCoordinator.d.ts +2 -14
  207. package/dist/runtime/runRetryCoordinator.js +3 -67
  208. package/dist/runtime/sandboxContainer.d.ts +7 -0
  209. package/dist/runtime/sandboxContainer.js +48 -6
  210. package/dist/runtime/sandboxDesk.d.ts +2 -1
  211. package/dist/runtime/sandboxDesk.js +11 -12
  212. package/dist/runtime/stageEnvironment.js +1 -0
  213. package/dist/runtime/stageRoots.d.ts +12 -0
  214. package/dist/runtime/stageSandbox.d.ts +2 -1
  215. package/dist/runtime/stageSandbox.js +18 -2
  216. package/dist/runtime/stageWorker.js +20 -0
  217. package/dist/server/agentAuthorRoutes.js +12 -3
  218. package/dist/server/bootstrap.d.ts +5 -0
  219. package/dist/server/bootstrap.js +14 -1
  220. package/dist/server/evalRoutes.js +38 -9
  221. package/dist/server/guideRoutes.d.ts +5 -0
  222. package/dist/server/guideRoutes.js +166 -0
  223. package/dist/server/http.d.ts +10 -0
  224. package/dist/server/http.js +35 -1
  225. package/dist/server/internalAgentRoutes.d.ts +6 -0
  226. package/dist/server/internalAgentRoutes.js +12 -0
  227. package/dist/server/mcpHost.js +2 -0
  228. package/dist/toolkit/meta.d.ts +1 -1
  229. package/dist/toolkit/meta.js +39 -1
  230. package/dist/toolkit/profiles.d.ts +5 -0
  231. package/dist/toolkit/profiles.js +12 -0
  232. package/dist/tools/askOperator.js +2 -1
  233. package/dist/tools/emitStageEnvelope.d.ts +17 -16
  234. package/dist/tools/emitStageEnvelope.js +46 -32
  235. package/dist/tools/stageToolNames.d.ts +4 -0
  236. package/dist/tools/stageToolNames.js +4 -0
  237. package/dist/tools/workingState.d.ts +40 -0
  238. package/dist/tools/workingState.js +108 -0
  239. package/dist/tools/writeStageArtifact.d.ts +0 -9
  240. package/dist/tools/writeStageArtifact.js +2 -1
  241. package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-S5C_vemc.js} +2 -2
  242. package/dist/ui/assets/adapters-C0l4ruiR.js +17 -0
  243. package/dist/ui/assets/chat-CjnsmvqR.js +2 -0
  244. package/dist/ui/assets/chat-DFDd4UjS.css +2 -0
  245. package/dist/ui/assets/{main-CHlmTNJe.css → main-Del1GeLf.css} +1 -1
  246. package/dist/ui/assets/main-_yXVqcW9.js +277 -0
  247. package/dist/ui/chat.html +3 -4
  248. package/dist/ui/index.html +3 -4
  249. package/dist/workshop/chatTurn.js +1 -1
  250. package/dist/workshop/workshopPin.d.ts +3 -3
  251. package/dist/workshop/workshopPin.js +16 -15
  252. package/examples/compaction-stress/README.md +30 -0
  253. package/examples/compaction-stress/check-run.mjs +53 -0
  254. package/examples/compaction-stress/collect-evidence.yaml +60 -0
  255. package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
  256. package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
  257. package/examples/compaction-stress/generate-evidence.mjs +110 -0
  258. package/examples/compaction-stress/root-cause.yaml +26 -0
  259. package/examples/compaction-stress/stageflow.yaml +9 -0
  260. package/examples/compaction-stress/write-postmortem.yaml +40 -0
  261. package/examples/evals/README.md +18 -0
  262. package/examples/evals/guide-behavior.dataset.jsonl +15 -0
  263. package/examples/evals/guide-behavior.eval.yaml +35 -0
  264. package/examples/evals/guide-depth.dataset.jsonl +35 -0
  265. package/examples/evals/guide-depth.eval.yaml +169 -0
  266. package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
  267. package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
  268. package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
  269. package/examples/memory/evals/guide-depth.eval.yaml +169 -0
  270. package/package.json +1 -1
  271. package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
  272. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
  273. package/dist/ui/assets/chat-Df_wCpak.js +0 -5
  274. package/dist/ui/assets/chat-P7KQznHm.css +0 -2
  275. package/dist/ui/assets/main-CvVfxmMd.js +0 -271
@@ -0,0 +1,19 @@
1
+ export const RUN_CLOCK_TERMINAL_STATUSES = [
2
+ "succeeded",
3
+ "failed",
4
+ "cancelled",
5
+ ];
6
+ const TERMINAL = new Set(RUN_CLOCK_TERMINAL_STATUSES);
7
+ export function isRunClockTerminal(status) {
8
+ return status !== undefined && TERMINAL.has(status);
9
+ }
10
+ export function nextFinishedAt(input) {
11
+ if (isRunClockTerminal(input.nextStatus)) {
12
+ if (input.currentFinishedAt != null &&
13
+ input.currentFinishedAt !== "") {
14
+ return { action: "keep" };
15
+ }
16
+ return { action: "stamp", finishedAt: input.now };
17
+ }
18
+ return { action: "clear" };
19
+ }
@@ -32,6 +32,7 @@ export declare class SqliteRunStore implements RunStore {
32
32
  findRunByDispatchKey(dispatchKey: string): Promise<CreatedRun | undefined>;
33
33
  private readSubmission;
34
34
  getRunBySubmission(key: string): Promise<RunSubmissionRecord | null>;
35
+ private applyRunStatusUpdate;
35
36
  updateRunStatus(runId: string, status: RunStatus): Promise<void>;
36
37
  tryUpdateRunStatus(runId: string, status: RunStatus, expectedStatus: RunStatus): Promise<boolean>;
37
38
  patchRunWorkspaceBinding(runId: string, patch: {
@@ -15,6 +15,7 @@ import { applyPendingMigrations, assertSchemaVersion, } from "./migrations/index
15
15
  import { StoreSchemaError } from "./storeSchemaError.js";
16
16
  import { applyStorePragmas } from "./applyStorePragmas.js";
17
17
  import { RunSubmissionExistsError } from "../submission.js";
18
+ import { nextFinishedAt } from "../runStatusClock.js";
18
19
  const EXECUTION_SELECT_COLS = `run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json, cost_usd, usage_json, auto_resume_count`;
19
20
  const ensuredStageDirs = new Set();
20
21
  function executionFromRow(row) {
@@ -289,58 +290,57 @@ export class SqliteRunStore {
289
290
  await this.ready();
290
291
  return this.readSubmission(key);
291
292
  }
293
+ applyRunStatusUpdate(runId, status, expectedStatus) {
294
+ const now = new Date().toISOString();
295
+ const current = this.db
296
+ .prepare(`SELECT finished_at FROM runs WHERE run_id = ?`)
297
+ .get(runId);
298
+ if (current === undefined) {
299
+ throw new Error(`Run not found: ${runId}`);
300
+ }
301
+ const decision = nextFinishedAt({
302
+ nextStatus: status,
303
+ currentFinishedAt: current.finished_at,
304
+ now,
305
+ });
306
+ const params = {
307
+ run_id: runId,
308
+ status,
309
+ updated_at: now,
310
+ };
311
+ let finishedSql;
312
+ if (decision.action === "stamp") {
313
+ finishedSql = "finished_at = @finished_at";
314
+ params.finished_at = decision.finishedAt;
315
+ }
316
+ else if (decision.action === "clear") {
317
+ finishedSql = "finished_at = NULL";
318
+ }
319
+ else {
320
+ finishedSql = "finished_at = finished_at";
321
+ }
322
+ const expectedClause = expectedStatus !== undefined
323
+ ? " AND status = @expected_status"
324
+ : "";
325
+ if (expectedStatus !== undefined) {
326
+ params.expected_status = expectedStatus;
327
+ }
328
+ const result = this.db
329
+ .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at, ${finishedSql}
330
+ WHERE run_id = @run_id${expectedClause}`)
331
+ .run(params);
332
+ return result.changes;
333
+ }
292
334
  async updateRunStatus(runId, status) {
293
335
  await this.ready();
294
- const now = new Date().toISOString();
295
- const isTerminal = status === "succeeded" || status === "failed" || status === "cancelled";
296
- const result = isTerminal
297
- ? this.db
298
- .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at,
299
- finished_at = COALESCE(finished_at, @finished_at)
300
- WHERE run_id = @run_id`)
301
- .run({
302
- run_id: runId,
303
- status,
304
- updated_at: now,
305
- finished_at: now,
306
- })
307
- : this.db
308
- .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at WHERE run_id = @run_id`)
309
- .run({
310
- run_id: runId,
311
- status,
312
- updated_at: now,
313
- });
314
- if (result.changes === 0) {
336
+ if (this.applyRunStatusUpdate(runId, status, undefined) === 0) {
315
337
  throw new Error(`Run not found: ${runId}`);
316
338
  }
317
339
  }
318
340
  async tryUpdateRunStatus(runId, status, expectedStatus) {
319
341
  await this.ready();
320
- const now = new Date().toISOString();
321
- const isTerminal = status === "succeeded" || status === "failed" || status === "cancelled";
322
- const result = isTerminal
323
- ? this.db
324
- .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at,
325
- finished_at = COALESCE(finished_at, @finished_at)
326
- WHERE run_id = @run_id AND status = @expected_status`)
327
- .run({
328
- run_id: runId,
329
- status,
330
- expected_status: expectedStatus,
331
- updated_at: now,
332
- finished_at: now,
333
- })
334
- : this.db
335
- .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at
336
- WHERE run_id = @run_id AND status = @expected_status`)
337
- .run({
338
- run_id: runId,
339
- status,
340
- expected_status: expectedStatus,
341
- updated_at: now,
342
- });
343
- if (result.changes > 0)
342
+ const changes = this.applyRunStatusUpdate(runId, status, expectedStatus);
343
+ if (changes > 0)
344
344
  return true;
345
345
  const exists = this.db
346
346
  .prepare(`SELECT 1 AS ok FROM runs WHERE run_id = ?`)
@@ -1,6 +1,7 @@
1
1
  import { access, lstat, mkdir, readdir, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { globalCredentialRoot } from "../project/globalHome.js";
4
+ import { buildArtifactListedPath } from "../runtime/artifactPath.js";
4
5
  import { STAGEFLOW_AGENT_AUTH_PATH_ENV } from "../runtime/credentialBinding.js";
5
6
  import { SF_STAGE_WORKER } from "../runtime/stageWorkerProtocol.js";
6
7
  export { stageDir } from "./paths.js";
@@ -186,7 +187,11 @@ export function resolveArtifactTarget(runWorkspaceDir, stageId, attempt, relativ
186
187
  path.isAbsolute(relativeToArtifacts)) {
187
188
  throw new Error("path escapes the stage artifacts directory");
188
189
  }
189
- const runRelativePath = path.posix.join("stages", stageId, "attempts", String(attempt), "artifacts", ...relativeToArtifacts.split(path.sep));
190
+ const runRelativePath = buildArtifactListedPath({
191
+ stageId,
192
+ attempt,
193
+ relative: relativeToArtifacts.split(path.sep).join("/"),
194
+ });
190
195
  return {
191
196
  absolutePath: resolved,
192
197
  runRelativePath,
@@ -0,0 +1,14 @@
1
+ export type ArtifactDiskFileType = "file" | "directory" | "symlink" | "other" | "missing";
2
+ export type ArtifactDiskPresence = {
3
+ file_type: ArtifactDiskFileType;
4
+ /** Lexical absolute path under root when the walk reached a leaf. */
5
+ absolutePath?: string;
6
+ /** From lstat when `file_type` is `file`. */
7
+ size_bytes?: number;
8
+ };
9
+ /**
10
+ * Closed-posture walk of `relativePath` under `rootDir`: lstat every segment,
11
+ * classify symlink / missing / file / directory / other. Does not follow
12
+ * symlinks. Escape attempts are `missing`. Non-ENOENT errors propagate.
13
+ */
14
+ export declare function inspectArtifactUnderRoot(rootDir: string, relativePath: string): Promise<ArtifactDiskPresence>;
@@ -0,0 +1,52 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ /**
4
+ * Closed-posture walk of `relativePath` under `rootDir`: lstat every segment,
5
+ * classify symlink / missing / file / directory / other. Does not follow
6
+ * symlinks. Escape attempts are `missing`. Non-ENOENT errors propagate.
7
+ */
8
+ export async function inspectArtifactUnderRoot(rootDir, relativePath) {
9
+ const target = path.resolve(rootDir, relativePath);
10
+ const relative = path.relative(rootDir, target);
11
+ if (relative === "" ||
12
+ relative === ".." ||
13
+ relative.startsWith(`..${path.sep}`) ||
14
+ path.isAbsolute(relative)) {
15
+ return { file_type: "missing" };
16
+ }
17
+ try {
18
+ const segments = relative.split(path.sep).filter((segment) => segment !== "");
19
+ let current = rootDir;
20
+ let stats;
21
+ for (const segment of segments) {
22
+ current = path.join(current, segment);
23
+ stats = await lstat(current);
24
+ if (stats.isSymbolicLink()) {
25
+ return { file_type: "symlink", absolutePath: current };
26
+ }
27
+ if (current !== target && !stats.isDirectory()) {
28
+ return { file_type: "other", absolutePath: current };
29
+ }
30
+ }
31
+ if (stats === undefined) {
32
+ return { file_type: "missing" };
33
+ }
34
+ if (stats.isDirectory()) {
35
+ return { file_type: "directory", absolutePath: current };
36
+ }
37
+ if (!stats.isFile()) {
38
+ return { file_type: "other", absolutePath: current };
39
+ }
40
+ return {
41
+ file_type: "file",
42
+ absolutePath: current,
43
+ size_bytes: stats.size,
44
+ };
45
+ }
46
+ catch (error) {
47
+ if (error.code === "ENOENT") {
48
+ return { file_type: "missing" };
49
+ }
50
+ throw error;
51
+ }
52
+ }
@@ -0,0 +1,24 @@
1
+ /** Parts of a run-relative artifact path under `stages/.../artifacts/`. */
2
+ export type ArtifactPathParts = {
3
+ stageId: string;
4
+ attempt: number;
5
+ relative: string;
6
+ };
7
+ export type ArtifactPathMode = {
8
+ kind: "grantable";
9
+ } | {
10
+ kind: "current_attempt";
11
+ stageId: string;
12
+ attempt: number;
13
+ };
14
+ export declare function attemptArtifactsPrefix(stageId: string, attempt: number): string;
15
+ export declare function buildArtifactListedPath(parts: ArtifactPathParts): string;
16
+ /**
17
+ * Accept a listed artifact path under a mode.
18
+ * - `grantable`: full run-relative path to a prior stage/attempt artifact file.
19
+ * - `current_attempt`: that attempt's run-relative prefix, or a bare path under
20
+ * its artifacts dir; foreign `stages/...` paths are rejected.
21
+ */
22
+ export declare function acceptArtifactListedPath(listedPath: string, mode: ArtifactPathMode): ArtifactPathParts | undefined;
23
+ export declare function parseArtifactListedPath(listedPath: string): ArtifactPathParts | undefined;
24
+ export declare function isGrantableArtifactPath(listedPath: string): boolean;
@@ -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[]>;