stageflow 0.25.0 → 0.27.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 (501) hide show
  1. package/README.md +10 -3
  2. package/dist/a2a/contracts.d.ts +1 -0
  3. package/dist/a2a/contracts.js +1 -0
  4. package/dist/a2a/registry.d.ts +2 -0
  5. package/dist/a2a/registry.js +20 -1
  6. package/dist/a2a/server.d.ts +7 -0
  7. package/dist/a2a/server.js +5 -2
  8. package/dist/a2a/service.d.ts +1 -1
  9. package/dist/a2a/service.js +57 -8
  10. package/dist/a2a/store.d.ts +8 -5
  11. package/dist/a2a/store.js +24 -67
  12. package/dist/agent/activity.d.ts +3 -0
  13. package/dist/agent/bootProviderConfig.d.ts +21 -0
  14. package/dist/agent/bootProviderConfig.js +74 -0
  15. package/dist/agent/piAdapter.d.ts +9 -1
  16. package/dist/agent/piAdapter.js +58 -3
  17. package/dist/agent/piIsolatedMcp.d.ts +2 -0
  18. package/dist/agent/piIsolatedMcp.js +10 -2
  19. package/dist/agent/port.d.ts +6 -0
  20. package/dist/agent/streamLogRedact.d.ts +1 -1
  21. package/dist/agent/streamLogRedact.js +4 -11
  22. package/dist/cli/artifactCommand.js +9 -16
  23. package/dist/cli/backupCommand.d.ts +9 -0
  24. package/dist/cli/backupCommand.js +111 -0
  25. package/dist/cli/debugRunCommand.d.ts +10 -0
  26. package/dist/cli/debugRunCommand.js +109 -0
  27. package/dist/cli/doctorCommand.d.ts +32 -0
  28. package/dist/cli/doctorCommand.js +453 -0
  29. package/dist/cli/envelopeCommand.js +9 -2
  30. package/dist/cli/exportAllCommand.d.ts +55 -0
  31. package/dist/cli/exportAllCommand.js +218 -0
  32. package/dist/cli/exportRunCommand.js +23 -21
  33. package/dist/cli/hostClient.d.ts +21 -3
  34. package/dist/cli/hostClient.js +93 -12
  35. package/dist/cli/initTemplates.d.ts +1 -1
  36. package/dist/cli/initTemplates.js +8 -1
  37. package/dist/cli/resolveSafeOutPath.d.ts +2 -0
  38. package/dist/cli/resolveSafeOutPath.js +40 -0
  39. package/dist/cli/restoreCommand.d.ts +10 -0
  40. package/dist/cli/restoreCommand.js +90 -0
  41. package/dist/cli/runCommand.d.ts +3 -2
  42. package/dist/cli/runCommand.js +93 -11
  43. package/dist/cli/runOutput.d.ts +4 -0
  44. package/dist/cli/runOutput.js +33 -7
  45. package/dist/cli/runStageCommand.d.ts +1 -0
  46. package/dist/cli/runStageCommand.js +44 -25
  47. package/dist/cli/runsCommand.d.ts +1 -1
  48. package/dist/cli/runsCommand.js +162 -8
  49. package/dist/cli.js +171 -30
  50. package/dist/config/catalogRelativePath.d.ts +62 -0
  51. package/dist/config/catalogRelativePath.js +118 -0
  52. package/dist/config/configOrigin.d.ts +27 -0
  53. package/dist/config/configOrigin.js +59 -0
  54. package/dist/config/hostConfig.d.ts +55 -0
  55. package/dist/config/hostConfig.js +333 -0
  56. package/dist/config/listSkills.d.ts +26 -0
  57. package/dist/config/listSkills.js +190 -3
  58. package/dist/config/loadOutcome.d.ts +1 -1
  59. package/dist/config/loadPipeline.js +23 -2
  60. package/dist/config/loadStage.js +18 -0
  61. package/dist/config/loadTask.d.ts +2 -0
  62. package/dist/config/loadTask.js +57 -18
  63. package/dist/config/materializeStageModels.js +5 -1
  64. package/dist/config/mergePipelineIncludes.d.ts +3 -0
  65. package/dist/config/mergePipelineIncludes.js +19 -0
  66. package/dist/config/migrateYaml.js +6 -11
  67. package/dist/config/multiProjectCatalog.d.ts +30 -0
  68. package/dist/config/multiProjectCatalog.js +142 -0
  69. package/dist/config/normalizePipelineStageEntry.js +20 -1
  70. package/dist/config/pipelineStageKeys.js +2 -0
  71. package/dist/config/resolveCatalogRoots.d.ts +27 -0
  72. package/dist/config/resolveCatalogRoots.js +76 -0
  73. package/dist/config/resolveModel.d.ts +7 -1
  74. package/dist/config/resolveModel.js +11 -1
  75. package/dist/config/resolveStageMcpServers.d.ts +9 -1
  76. package/dist/config/resolveStageMcpServers.js +36 -9
  77. package/dist/config/secretFromEnvOrFile.d.ts +9 -0
  78. package/dist/config/secretFromEnvOrFile.js +34 -0
  79. package/dist/config/seededCatalog.d.ts +11 -0
  80. package/dist/config/seededCatalog.js +29 -0
  81. package/dist/config/toolRequires.d.ts +24 -0
  82. package/dist/config/toolRequires.js +115 -0
  83. package/dist/config/validateCatalog.d.ts +5 -1
  84. package/dist/config/validateCatalog.js +140 -1
  85. package/dist/diagnostics/checks.d.ts +39 -0
  86. package/dist/diagnostics/checks.js +135 -0
  87. package/dist/errors/codes.d.ts +14 -0
  88. package/dist/errors/codes.js +59 -0
  89. package/dist/git/cache.d.ts +14 -0
  90. package/dist/git/cache.js +150 -0
  91. package/dist/git/credentials.d.ts +9 -0
  92. package/dist/git/credentials.js +88 -0
  93. package/dist/git/errors.d.ts +20 -0
  94. package/dist/git/errors.js +94 -0
  95. package/dist/git/exec.d.ts +16 -0
  96. package/dist/git/exec.js +135 -0
  97. package/dist/git/operations.d.ts +29 -0
  98. package/dist/git/operations.js +153 -0
  99. package/dist/git/version.d.ts +1 -0
  100. package/dist/git/version.js +10 -0
  101. package/dist/logging/audit.d.ts +16 -0
  102. package/dist/logging/audit.js +19 -0
  103. package/dist/logging/commonWords.d.ts +2 -0
  104. package/dist/logging/commonWords.js +13 -0
  105. package/dist/logging/logger.d.ts +49 -0
  106. package/dist/logging/logger.js +193 -0
  107. package/dist/logging/namedSecrets.d.ts +6 -0
  108. package/dist/logging/namedSecrets.js +58 -0
  109. package/dist/logging/redact.d.ts +13 -0
  110. package/dist/logging/redact.js +88 -0
  111. package/dist/mcp/catalogTools.js +456 -80
  112. package/dist/mcp/checkoutTools.d.ts +52 -0
  113. package/dist/mcp/checkoutTools.js +332 -0
  114. package/dist/mcp/controlTools.js +179 -5
  115. package/dist/mcp/deps.d.ts +1 -0
  116. package/dist/mcp/getStartedTool.d.ts +3 -0
  117. package/dist/mcp/getStartedTool.js +62 -0
  118. package/dist/mcp/readArtifact.d.ts +11 -0
  119. package/dist/mcp/readArtifact.js +46 -30
  120. package/dist/mcp/server.js +1 -0
  121. package/dist/mcp/tools.js +4 -0
  122. package/dist/net/proxy.d.ts +14 -0
  123. package/dist/net/proxy.js +78 -0
  124. package/dist/package-meta.d.ts +2 -1
  125. package/dist/package-meta.js +2 -1
  126. package/dist/preflight/bash.d.ts +8 -0
  127. package/dist/preflight/bash.js +34 -0
  128. package/dist/preflight/claudeRoot.d.ts +11 -0
  129. package/dist/preflight/claudeRoot.js +23 -0
  130. package/dist/preflight/mcpCommands.d.ts +8 -0
  131. package/dist/preflight/mcpCommands.js +36 -0
  132. package/dist/preflight/pipelinePreflight.d.ts +40 -0
  133. package/dist/preflight/pipelinePreflight.js +192 -0
  134. package/dist/preflight/tls.d.ts +6 -0
  135. package/dist/preflight/tls.js +18 -0
  136. package/dist/preflight/toolchain.d.ts +45 -0
  137. package/dist/preflight/toolchain.js +219 -0
  138. package/dist/project/findProjectRoot.js +3 -4
  139. package/dist/project/globalHome.d.ts +7 -0
  140. package/dist/project/globalHome.js +63 -2
  141. package/dist/project/resolveProjectContext.js +2 -2
  142. package/dist/project/resolveStageflowContext.js +2 -2
  143. package/dist/projection/projectRun.d.ts +2 -1
  144. package/dist/projection/projectRun.js +1 -0
  145. package/dist/runstore/assertTmpdir.d.ts +1 -0
  146. package/dist/runstore/assertTmpdir.js +21 -0
  147. package/dist/runstore/backup.d.ts +52 -0
  148. package/dist/runstore/backup.js +292 -0
  149. package/dist/runstore/createStore.d.ts +18 -1
  150. package/dist/runstore/createStore.js +64 -5
  151. package/dist/runstore/debugBundle.d.ts +60 -0
  152. package/dist/runstore/debugBundle.js +143 -0
  153. package/dist/runstore/diskUsage.d.ts +50 -0
  154. package/dist/runstore/diskUsage.js +214 -0
  155. package/dist/runstore/exportRunPayload.d.ts +8 -0
  156. package/dist/runstore/exportRunPayload.js +23 -0
  157. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  158. package/dist/runstore/normalizeCatalogPath.js +10 -0
  159. package/dist/runstore/paths.d.ts +5 -0
  160. package/dist/runstore/paths.js +145 -1
  161. package/dist/runstore/port.d.ts +111 -13
  162. package/dist/runstore/port.js +8 -1
  163. package/dist/runstore/restore.d.ts +81 -0
  164. package/dist/runstore/restore.js +355 -0
  165. package/dist/runstore/retention.d.ts +27 -0
  166. package/dist/runstore/retention.js +74 -0
  167. package/dist/runstore/runManifest.d.ts +146 -0
  168. package/dist/runstore/runManifest.js +288 -0
  169. package/dist/runstore/runProjection.d.ts +3 -1
  170. package/dist/runstore/runProjection.js +63 -1
  171. package/dist/runstore/sqlite/SqliteRunStore.d.ts +28 -3
  172. package/dist/runstore/sqlite/SqliteRunStore.js +352 -219
  173. package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
  174. package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
  175. package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
  176. package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
  177. package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
  178. package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
  179. package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
  180. package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
  181. package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
  182. package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
  183. package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
  184. package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
  185. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
  186. package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
  187. package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
  188. package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
  189. package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
  190. package/dist/runstore/sqlite/migrations/index.js +72 -0
  191. package/dist/runstore/sqlite/schema.d.ts +20 -1
  192. package/dist/runstore/sqlite/schema.js +277 -1
  193. package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
  194. package/dist/runstore/sqlite/storeOpenError.js +11 -0
  195. package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
  196. package/dist/runstore/sqlite/storeSchemaError.js +8 -0
  197. package/dist/runstore/stageExecution.js +3 -0
  198. package/dist/runstore/storeFilesystem.d.ts +32 -0
  199. package/dist/runstore/storeFilesystem.js +148 -0
  200. package/dist/runstore/trackProjection.js +2 -0
  201. package/dist/runstore/workspaceLayout.d.ts +13 -0
  202. package/dist/runstore/workspaceLayout.js +76 -0
  203. package/dist/runtime/completionCheckRunner.d.ts +3 -0
  204. package/dist/runtime/completionCheckRunner.js +45 -7
  205. package/dist/runtime/containerLimits.d.ts +16 -0
  206. package/dist/runtime/containerLimits.js +84 -0
  207. package/dist/runtime/credentialMaterialisation.d.ts +28 -0
  208. package/dist/runtime/credentialMaterialisation.js +128 -0
  209. package/dist/runtime/gitCheckoutCapability.js +7 -13
  210. package/dist/runtime/pipelineRunner.d.ts +36 -1
  211. package/dist/runtime/pipelineRunner.js +178 -17
  212. package/dist/runtime/pipelineScheduler.d.ts +15 -0
  213. package/dist/runtime/pipelineScheduler.js +135 -27
  214. package/dist/runtime/reloadRunCatalog.js +6 -1
  215. package/dist/runtime/repositoryMaterialize.d.ts +47 -0
  216. package/dist/runtime/repositoryMaterialize.js +235 -0
  217. package/dist/runtime/resumeTimedOut.d.ts +1 -1
  218. package/dist/runtime/resumeTimedOut.js +5 -2
  219. package/dist/runtime/runChangeBus.js +9 -0
  220. package/dist/runtime/runDeletion.d.ts +7 -0
  221. package/dist/runtime/runDeletion.js +37 -0
  222. package/dist/runtime/runManager.d.ts +158 -4
  223. package/dist/runtime/runManager.js +1596 -79
  224. package/dist/runtime/runRetentionSweep.d.ts +31 -0
  225. package/dist/runtime/runRetentionSweep.js +328 -0
  226. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  227. package/dist/runtime/runRetryCoordinator.js +19 -2
  228. package/dist/runtime/runSkills.d.ts +40 -0
  229. package/dist/runtime/runSkills.js +173 -0
  230. package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
  231. package/dist/runtime/stageAttemptBootstrap.js +195 -24
  232. package/dist/runtime/stageCacheEnv.d.ts +6 -0
  233. package/dist/runtime/stageCacheEnv.js +34 -0
  234. package/dist/runtime/stageConcurrency.d.ts +2 -1
  235. package/dist/runtime/stageConcurrency.js +4 -2
  236. package/dist/runtime/stageEnvironment.d.ts +38 -0
  237. package/dist/runtime/stageEnvironment.js +193 -0
  238. package/dist/runtime/stageProcessLauncher.d.ts +18 -0
  239. package/dist/runtime/stageProcessLauncher.js +194 -38
  240. package/dist/runtime/stageRecovery.d.ts +3 -1
  241. package/dist/runtime/stageRecovery.js +19 -4
  242. package/dist/runtime/stageRoots.d.ts +38 -1
  243. package/dist/runtime/stageRoots.js +118 -0
  244. package/dist/runtime/stageRunner.d.ts +1 -0
  245. package/dist/runtime/stageRunner.js +3 -1
  246. package/dist/runtime/stageSecretDecl.d.ts +9 -0
  247. package/dist/runtime/stageSecretDecl.js +102 -0
  248. package/dist/runtime/stageSecrets.d.ts +40 -0
  249. package/dist/runtime/stageSecrets.js +181 -0
  250. package/dist/runtime/stageWorker.d.ts +3 -2
  251. package/dist/runtime/stageWorker.js +58 -10
  252. package/dist/runtime/stageWorkerProtocol.d.ts +5 -0
  253. package/dist/runtime/stageWorkerProtocol.js +8 -0
  254. package/dist/runtime/startPayload.d.ts +32 -0
  255. package/dist/runtime/startPayload.js +59 -0
  256. package/dist/runtime/taskInput.js +14 -5
  257. package/dist/runtime/verifiedStageExecution.d.ts +1 -0
  258. package/dist/runtime/verifiedStageExecution.js +2 -1
  259. package/dist/runtime/workspaceBinding.d.ts +20 -0
  260. package/dist/runtime/workspaceBinding.js +60 -0
  261. package/dist/server/allowedHosts.d.ts +19 -0
  262. package/dist/server/allowedHosts.js +179 -0
  263. package/dist/server/bootstrap.d.ts +36 -2
  264. package/dist/server/bootstrap.js +209 -5
  265. package/dist/server/controlToken.d.ts +47 -0
  266. package/dist/server/controlToken.js +241 -0
  267. package/dist/server/createHttpHost.d.ts +11 -2
  268. package/dist/server/createHttpHost.js +103 -10
  269. package/dist/server/ensureGlobalService.d.ts +5 -1
  270. package/dist/server/ensureGlobalService.js +35 -1
  271. package/dist/server/healthSurfaces.d.ts +6 -0
  272. package/dist/server/healthSurfaces.js +98 -0
  273. package/dist/server/http.d.ts +12 -1
  274. package/dist/server/http.js +1085 -485
  275. package/dist/server/listenHost.d.ts +5 -0
  276. package/dist/server/listenHost.js +56 -0
  277. package/dist/server/mcpHost.d.ts +10 -1
  278. package/dist/server/mcpHost.js +36 -6
  279. package/dist/server/operatorResults.d.ts +2 -2
  280. package/dist/server/operatorResults.js +40 -29
  281. package/dist/server/requestAuthContext.d.ts +12 -0
  282. package/dist/server/requestAuthContext.js +21 -0
  283. package/dist/server/shutdown.d.ts +69 -0
  284. package/dist/server/shutdown.js +219 -0
  285. package/dist/types/pipeline.d.ts +7 -0
  286. package/dist/types/stage.d.ts +8 -0
  287. package/dist/types/task.d.ts +8 -0
  288. package/dist/ui/assets/index-B6hlSXXn.js +120 -0
  289. package/dist/ui/index.html +1 -1
  290. package/examples/README.md +98 -0
  291. package/examples/archify-on-pr/README.md +213 -0
  292. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +19 -0
  293. package/examples/archify-on-pr/archify-on-pr.task.yaml +19 -0
  294. package/examples/archify-on-pr/author-diagrams.yaml +106 -0
  295. package/examples/archify-on-pr/detect-changes.yaml +89 -0
  296. package/examples/ci-validate/README.md +35 -0
  297. package/examples/ci-validate/check.yaml +13 -0
  298. package/examples/ci-validate/ci-demo.pipeline.yaml +4 -0
  299. package/examples/ci-validate/validate.sh +5 -0
  300. package/examples/compliance-review-demo/README.md +125 -0
  301. package/examples/compliance-review-demo/compliance-review-demo.pipeline.yaml +29 -0
  302. package/examples/compliance-review-demo/connect-account.yaml +47 -0
  303. package/examples/compliance-review-demo/map-to-control.yaml +123 -0
  304. package/examples/compliance-review-demo/poll-bucket-config.yaml +103 -0
  305. package/examples/compliance-review-demo/publish-report.yaml +79 -0
  306. package/examples/compliance-review-demo/review-verdict.yaml +122 -0
  307. package/examples/compliance-review-demo/run-compliance-check.yaml +96 -0
  308. package/examples/compliance-review-demo/store-evidence.yaml +143 -0
  309. package/examples/conditional-fork/README.md +89 -0
  310. package/examples/conditional-fork/branch-a.yaml +13 -0
  311. package/examples/conditional-fork/branch-b.yaml +13 -0
  312. package/examples/conditional-fork/decide.yaml +35 -0
  313. package/examples/conditional-fork/fork-demo.pipeline.yaml +20 -0
  314. package/examples/conditional-fork/fork-demo.task.yaml +8 -0
  315. package/examples/context7-mcp/README.md +42 -0
  316. package/examples/context7-mcp/context7-mcp.pipeline.yaml +15 -0
  317. package/examples/context7-mcp/context7-mcp.task.yaml +4 -0
  318. package/examples/context7-mcp/fetch-docs.yaml +52 -0
  319. package/examples/context7-mcp/resolve-library.yaml +31 -0
  320. package/examples/context7-mcp/write-brief.yaml +52 -0
  321. package/examples/feature-loop/README.md +104 -0
  322. package/examples/feature-loop/address-feedback.yaml +148 -0
  323. package/examples/feature-loop/align.yaml +116 -0
  324. package/examples/feature-loop/decompose.yaml +86 -0
  325. package/examples/feature-loop/default-model-hierarchy.task.yaml +58 -0
  326. package/examples/feature-loop/feature-loop.pipeline.yaml +140 -0
  327. package/examples/feature-loop/feature-loop.task.yaml +25 -0
  328. package/examples/feature-loop/implement.yaml +128 -0
  329. package/examples/feature-loop/plan.yaml +111 -0
  330. package/examples/feature-loop/publish.yaml +167 -0
  331. package/examples/feature-loop/review.yaml +146 -0
  332. package/examples/feature-loop/verify.yaml +106 -0
  333. package/examples/feedback-loop/README.md +72 -0
  334. package/examples/feedback-loop/VERIFY.md +208 -0
  335. package/examples/feedback-loop/continue-only.task.yaml +4 -0
  336. package/examples/feedback-loop/exhaust-require-continue.task.yaml +6 -0
  337. package/examples/feedback-loop/exhaust-wait-human.task.yaml +11 -0
  338. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +23 -0
  339. package/examples/feedback-loop/feedback-loop.pipeline.yaml +23 -0
  340. package/examples/feedback-loop/feedback-loop.task.yaml +4 -0
  341. package/examples/feedback-loop/implement.yaml +14 -0
  342. package/examples/feedback-loop/plan.yaml +14 -0
  343. package/examples/feedback-loop/review.yaml +15 -0
  344. package/examples/feedback-loop/send-back-once.task.yaml +6 -0
  345. package/examples/feedback-loop/submit.yaml +12 -0
  346. package/examples/generic-fan-in/README.md +151 -0
  347. package/examples/generic-fan-in/clarify.yaml +18 -0
  348. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +19 -0
  349. package/examples/generic-fan-in/generic-fan-in-accepted.task.yaml +8 -0
  350. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +18 -0
  351. package/examples/generic-fan-in/generic-fan-in.task.yaml +9 -0
  352. package/examples/generic-fan-in/research.yaml +45 -0
  353. package/examples/generic-fan-in/synthesize.yaml +66 -0
  354. package/examples/generic-fan-in/validation.yaml +45 -0
  355. package/examples/github-release/README.md +78 -0
  356. package/examples/github-release/draft-release-notes.yaml +64 -0
  357. package/examples/github-release/github-release.pipeline.yaml +11 -0
  358. package/examples/github-release/github-release.task.yaml +13 -0
  359. package/examples/github-release/publish-github-release.yaml +64 -0
  360. package/examples/hello-world/README.md +28 -0
  361. package/examples/hello-world/hello.pipeline.yaml +4 -0
  362. package/examples/hello-world/my-task.task.yaml +6 -0
  363. package/examples/hello-world/research.yaml +18 -0
  364. package/examples/mcp-hitl-tour/README.md +113 -0
  365. package/examples/mcp-hitl-tour/draft-angle.yaml +33 -0
  366. package/examples/mcp-hitl-tour/draft-short.yaml +32 -0
  367. package/examples/mcp-hitl-tour/emit-angles.yaml +45 -0
  368. package/examples/mcp-hitl-tour/gather.yaml +31 -0
  369. package/examples/mcp-hitl-tour/intake.yaml +62 -0
  370. package/examples/mcp-hitl-tour/mcp-hitl-tour-exhaust.task.yaml +13 -0
  371. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +54 -0
  372. package/examples/mcp-hitl-tour/mcp-hitl-tour.task.yaml +16 -0
  373. package/examples/mcp-hitl-tour/polish.yaml +33 -0
  374. package/examples/mcp-hitl-tour/review.yaml +17 -0
  375. package/examples/mcp-hitl-tour/ship.yaml +25 -0
  376. package/examples/oss-issue-contribution/README.md +218 -0
  377. package/examples/oss-issue-contribution/oss-address-review-feedback.yaml +117 -0
  378. package/examples/oss-issue-contribution/oss-approve-contribution.yaml +181 -0
  379. package/examples/oss-issue-contribution/oss-approve-plan.yaml +111 -0
  380. package/examples/oss-issue-contribution/oss-explain-fix.yaml +108 -0
  381. package/examples/oss-issue-contribution/oss-explain-issue.yaml +114 -0
  382. package/examples/oss-issue-contribution/oss-implement-source-fix.yaml +171 -0
  383. package/examples/oss-issue-contribution/oss-investigate-area.yaml +116 -0
  384. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +125 -0
  385. package/examples/oss-issue-contribution/oss-issue-intake.yaml +106 -0
  386. package/examples/oss-issue-contribution/oss-plan-investigation.yaml +112 -0
  387. package/examples/oss-issue-contribution/oss-plan-review.yaml +109 -0
  388. package/examples/oss-issue-contribution/oss-reproduce-issue.yaml +140 -0
  389. package/examples/oss-issue-contribution/oss-review-change.yaml +107 -0
  390. package/examples/oss-issue-contribution/oss-verify-fix.yaml +130 -0
  391. package/examples/oss-issue-contribution/oss-write-regression-test.yaml +135 -0
  392. package/examples/oss-issue-contribution/pi-7629.task.yaml +79 -0
  393. package/examples/oss-issue-contribution/pi-8919.task.yaml +75 -0
  394. package/examples/oss-issue-contribution/pydantic-ai-7971.task.yaml +53 -0
  395. package/examples/oss-quick-fix/README.md +65 -0
  396. package/examples/oss-quick-fix/implement-fix.yaml +84 -0
  397. package/examples/oss-quick-fix/oss-quick-fix.pipeline.yaml +35 -0
  398. package/examples/oss-quick-fix/oss-quick-fix.task.yaml +19 -0
  399. package/examples/oss-quick-fix/ship-fix.yaml +81 -0
  400. package/examples/oss-quick-fix/understand-issue.yaml +67 -0
  401. package/examples/oss-quick-fix/verify-fix.yaml +73 -0
  402. package/examples/plan-review/README.md +37 -0
  403. package/examples/plan-review/plan-review-followup.yaml +13 -0
  404. package/examples/plan-review/plan-review.pipeline.yaml +9 -0
  405. package/examples/plan-review/plan-review.task.yaml +9 -0
  406. package/examples/plan-review/plan-review.yaml +25 -0
  407. package/examples/playwright-mcp/README.md +39 -0
  408. package/examples/playwright-mcp/playwright-mcp.pipeline.yaml +5 -0
  409. package/examples/playwright-mcp/playwright-mcp.task.yaml +4 -0
  410. package/examples/playwright-mcp/screenshot.yaml +40 -0
  411. package/examples/retry-tour/README.md +75 -0
  412. package/examples/retry-tour/retry-tour.pipeline.yaml +122 -0
  413. package/examples/retry-tour/retry-tour.task.yaml +9 -0
  414. package/examples/route-if-tour/README.md +202 -0
  415. package/examples/route-if-tour/route-if-tour.pipeline.yaml +950 -0
  416. package/examples/route-if-tour/route-if-tour.task.yaml +4 -0
  417. package/examples/route-wiring-smoke-test/01-core-routing.pipeline.yaml +168 -0
  418. package/examples/route-wiring-smoke-test/02-on-gating.pipeline.yaml +44 -0
  419. package/examples/route-wiring-smoke-test/03-two-entry-points.pipeline.yaml +28 -0
  420. package/examples/route-wiring-smoke-test/04-entry-false-equivalent-to-omitted.pipeline.yaml +27 -0
  421. package/examples/route-wiring-smoke-test/05-fork-choice.pipeline.yaml +110 -0
  422. package/examples/route-wiring-smoke-test/06-fan-out-fan-in.pipeline.yaml +82 -0
  423. package/examples/route-wiring-smoke-test/07-loop-human-decision.pipeline.yaml +82 -0
  424. package/examples/route-wiring-smoke-test/08-skip-fallback-and-multi-on-gate.pipeline.yaml +77 -0
  425. package/examples/route-wiring-smoke-test/09-multi-loop-targets.pipeline.yaml +102 -0
  426. package/examples/route-wiring-smoke-test/10-uses-dialect-fork.pipeline.yaml +23 -0
  427. package/examples/route-wiring-smoke-test/11-sequential-io-handoff.pipeline.yaml +37 -0
  428. package/examples/route-wiring-smoke-test/12-complex-io-schemas.pipeline.yaml +138 -0
  429. package/examples/route-wiring-smoke-test/13-ref-io-handoff.pipeline.yaml +84 -0
  430. package/examples/route-wiring-smoke-test/14-if-eq-gating.pipeline.yaml +50 -0
  431. package/examples/route-wiring-smoke-test/15-if-all-gated.pipeline.yaml +37 -0
  432. package/examples/route-wiring-smoke-test/16-if-composition.pipeline.yaml +65 -0
  433. package/examples/route-wiring-smoke-test/17-if-join.pipeline.yaml +80 -0
  434. package/examples/route-wiring-smoke-test/README.md +377 -0
  435. package/examples/route-wiring-smoke-test/rejected/01-reject-legacy-needs.pipeline.yaml +26 -0
  436. package/examples/route-wiring-smoke-test/rejected/02-reject-legacy-fork.pipeline.yaml +25 -0
  437. package/examples/route-wiring-smoke-test/rejected/03-reject-legacy-feedback-loop.pipeline.yaml +30 -0
  438. package/examples/route-wiring-smoke-test/rejected/04-reject-missing-entry.pipeline.yaml +24 -0
  439. package/examples/route-wiring-smoke-test/rejected/05-reject-unreachable.pipeline.yaml +35 -0
  440. package/examples/route-wiring-smoke-test/rejected/06-reject-allow-none-without-route-select.pipeline.yaml +37 -0
  441. package/examples/route-wiring-smoke-test/rejected/07-reject-cycle.pipeline.yaml +27 -0
  442. package/examples/route-wiring-smoke-test/rejected/08-reject-loop-non-ancestor.pipeline.yaml +42 -0
  443. package/examples/route-wiring-smoke-test/rejected/09-reject-route-select-leaf.pipeline.yaml +26 -0
  444. package/examples/route-wiring-smoke-test/rejected/12-reject-loop-replay-unsafe.pipeline.yaml +55 -0
  445. package/examples/route-wiring-smoke-test/rejected/13-reject-multiple-loop-entries.pipeline.yaml +51 -0
  446. package/examples/route-wiring-smoke-test/rejected/14-reject-duplicate-stage-id.pipeline.yaml +35 -0
  447. package/examples/route-wiring-smoke-test/rejected/15-reject-unknown-route-target.pipeline.yaml +15 -0
  448. package/examples/route-wiring-smoke-test/rejected/16-reject-malformed-route-item.pipeline.yaml +15 -0
  449. package/examples/route-wiring-smoke-test/rejected/17-reject-io-incompatible.pipeline.yaml +33 -0
  450. package/examples/route-wiring-smoke-test/rejected/18-reject-nested-io.pipeline.yaml +51 -0
  451. package/examples/route-wiring-smoke-test/rejected/19-reject-array-item-io.pipeline.yaml +55 -0
  452. package/examples/route-wiring-smoke-test/rejected/20-reject-closed-io.pipeline.yaml +36 -0
  453. package/examples/route-wiring-smoke-test/rejected/21-reject-ref-io.pipeline.yaml +42 -0
  454. package/examples/route-wiring-smoke-test/rejected/22-reject-if-unknown-field.pipeline.yaml +33 -0
  455. package/examples/route-wiring-smoke-test/rejected/23-reject-if-optional-field.pipeline.yaml +32 -0
  456. package/examples/route-wiring-smoke-test/rejected/24-reject-if-empty-all.pipeline.yaml +31 -0
  457. package/examples/route-wiring-smoke-test/rejected/25-reject-if-gt-on-string.pipeline.yaml +33 -0
  458. package/examples/route-wiring-smoke-test/rejected/26-reject-if-optional-nested.pipeline.yaml +36 -0
  459. package/examples/route-wiring-smoke-test/rejected/27-reject-if-on-loop.pipeline.yaml +63 -0
  460. package/examples/route-wiring-smoke-test/rejected/29-reject-if-on-failed.pipeline.yaml +46 -0
  461. package/examples/route-wiring-smoke-test/smoke-test.task.yaml +4 -0
  462. package/examples/route-wiring-smoke-test/stages/branch-a.yaml +12 -0
  463. package/examples/route-wiring-smoke-test/stages/branch-b.yaml +12 -0
  464. package/examples/route-wiring-smoke-test/stages/decide.yaml +12 -0
  465. package/examples/route-wiring-smoke-test/stages/finish.yaml +12 -0
  466. package/examples/ship-feature/README.md +190 -0
  467. package/examples/ship-feature/feat-address-feedback.yaml +119 -0
  468. package/examples/ship-feature/feat-implement.yaml +180 -0
  469. package/examples/ship-feature/feat-plan.yaml +120 -0
  470. package/examples/ship-feature/feat-review.yaml +109 -0
  471. package/examples/ship-feature/feat-ship.yaml +203 -0
  472. package/examples/ship-feature/ship-feature.pipeline.yaml +44 -0
  473. package/examples/ship-feature/ship-feature.task.yaml +78 -0
  474. package/examples/stage-mcp/README.md +144 -0
  475. package/examples/stage-mcp/echo-mcp.mjs +20 -0
  476. package/examples/stage-mcp/stage-mcp-dead.pipeline.yaml +5 -0
  477. package/examples/stage-mcp/stage-mcp-dead.task.yaml +3 -0
  478. package/examples/stage-mcp/stage-mcp-missing-var.pipeline.yaml +5 -0
  479. package/examples/stage-mcp/stage-mcp-missing-var.task.yaml +3 -0
  480. package/examples/stage-mcp/stage-mcp.pipeline.yaml +5 -0
  481. package/examples/stage-mcp/stage-mcp.task.yaml +4 -0
  482. package/examples/stage-mcp/use-echo.yaml +20 -0
  483. package/examples/verify-tour/README.md +80 -0
  484. package/examples/verify-tour/verify-tour.pipeline.yaml +162 -0
  485. package/examples/verify-tour/verify-tour.task.yaml +11 -0
  486. package/package.json +5 -1
  487. package/skills/stageflow/SKILL.md +6 -6
  488. package/skills/stageflow/references/control-surface.md +11 -5
  489. package/skills/stageflow/scripts/detect-host.mjs +14 -1
  490. package/skills/stageflow/scripts/detect-host.test.mjs +29 -1
  491. package/skills/stageflow-author/SKILL.md +4 -3
  492. package/skills/stageflow-author/references/catalog-mapping.md +16 -0
  493. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  494. package/skills/stageflow-run/SKILL.md +20 -12
  495. package/skills/stageflow-run/references/mcp-call.md +9 -1
  496. package/skills/stageflow-run/references/task-and-pipeline-selection.md +16 -5
  497. package/skills/stageflow-run/scripts/mcp-call.mjs +46 -6
  498. package/skills/stageflow-run/scripts/mcp-call.test.mjs +64 -0
  499. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -0
  500. package/skills/stageflow-setup/SKILL.md +40 -8
  501. package/dist/ui/assets/index-Da5EHU6M.js +0 -119
@@ -1,196 +1,20 @@
1
1
  import Database from "better-sqlite3";
2
+ import { existsSync, statSync } from "node:fs";
2
3
  import { mkdir } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { projectRunDetail, projectRunSummary, orderStageSnapshots } from "../runProjection.js";
5
- import { normalizeCatalogPath } from "../normalizeCatalogPath.js";
6
+ import { normalizeCatalogPath, normalizeProjectRoot } from "../normalizeCatalogPath.js";
6
7
  import { buildStageSnapshotFromStore } from "../stageSnapshot.js";
7
8
  import { parsePipelineDagSnapshot } from "../pipelineDagSnapshot.js";
8
9
  import { newRunId, runWorkspaceDir } from "../paths.js";
9
10
  import { attemptAgentDir, attemptArtifactsDir, stageDir, } from "../workspaceLayout.js";
10
11
  import { importDiskRunsIfEmpty } from "./migrateFromDisk.js";
11
- import { SCHEMA_SQL } from "./schema.js";
12
+ import { applyPendingMigrations, assertSchemaVersion, } from "./migrations/index.js";
13
+ import { StoreSchemaError } from "./storeSchemaError.js";
14
+ import { applyStorePragmas } from "./applyStorePragmas.js";
12
15
  import { RunSubmissionExistsError } from "../submission.js";
16
+ 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`;
13
17
  const ensuredStageDirs = new Set();
14
- const DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5000;
15
- function readSqliteBusyTimeoutMs() {
16
- const raw = process.env.STAGEFLOW_SQLITE_BUSY_TIMEOUT_MS;
17
- if (raw === undefined || raw.trim() === "") {
18
- return DEFAULT_SQLITE_BUSY_TIMEOUT_MS;
19
- }
20
- const parsed = Number.parseInt(raw, 10);
21
- if (!Number.isFinite(parsed) || parsed < 0) {
22
- return DEFAULT_SQLITE_BUSY_TIMEOUT_MS;
23
- }
24
- return parsed;
25
- }
26
- function ensureCheckoutRootColumn(db) {
27
- const cols = db.prepare(`PRAGMA table_info(runs)`).all();
28
- if (!cols.some((c) => c.name === "checkout_root")) {
29
- db.exec(`ALTER TABLE runs ADD COLUMN checkout_root TEXT`);
30
- }
31
- }
32
- function ensureCiIdentityColumns(db) {
33
- const cols = db.prepare(`PRAGMA table_info(runs)`).all();
34
- const names = new Set(cols.map((c) => c.name));
35
- for (const name of ["git_sha", "ci_pr_url", "ci_job_url"]) {
36
- if (!names.has(name)) {
37
- db.exec(`ALTER TABLE runs ADD COLUMN ${name} TEXT`);
38
- }
39
- }
40
- }
41
- function ensurePipelineDagColumn(db) {
42
- const cols = db.prepare(`PRAGMA table_info(runs)`).all();
43
- if (!cols.some((c) => c.name === "pipeline_dag_json")) {
44
- db.exec(`ALTER TABLE runs ADD COLUMN pipeline_dag_json TEXT`);
45
- }
46
- }
47
- function ensureRunLocatorColumns(db) {
48
- const cols = db.prepare(`PRAGMA table_info(runs)`).all();
49
- const names = new Set(cols.map((c) => c.name));
50
- for (const name of ["pipeline_path", "task_path", "project_root"]) {
51
- if (!names.has(name)) {
52
- db.exec(`ALTER TABLE runs ADD COLUMN ${name} TEXT`);
53
- }
54
- }
55
- }
56
- function ensureInlinePipelineColumn(db) {
57
- const cols = db.prepare(`PRAGMA table_info(runs)`).all();
58
- if (!cols.some((c) => c.name === "inline_pipeline_json")) {
59
- db.exec(`ALTER TABLE runs ADD COLUMN inline_pipeline_json TEXT`);
60
- }
61
- }
62
- function ensureStageExecutionsTable(db) {
63
- db.exec(`
64
- CREATE TABLE IF NOT EXISTS stage_executions (
65
- run_id TEXT NOT NULL,
66
- stage_id TEXT NOT NULL,
67
- attempt INTEGER NOT NULL,
68
- status TEXT NOT NULL,
69
- verification_outcome TEXT NOT NULL DEFAULT 'not_run',
70
- started_at TEXT,
71
- finished_at TEXT,
72
- envelope_json TEXT,
73
- PRIMARY KEY (run_id, stage_id, attempt),
74
- FOREIGN KEY (run_id) REFERENCES runs(run_id)
75
- );
76
- CREATE INDEX IF NOT EXISTS idx_stage_executions_run_stage
77
- ON stage_executions (run_id, stage_id, attempt);
78
- `);
79
- }
80
- function ensureStageExecutionVerificationOutcomeColumn(db) {
81
- const cols = db
82
- .prepare(`PRAGMA table_info(stage_executions)`)
83
- .all();
84
- if (!cols.some((c) => c.name === "verification_outcome")) {
85
- db.exec(`ALTER TABLE stage_executions ADD COLUMN verification_outcome TEXT NOT NULL DEFAULT 'not_run'`);
86
- }
87
- }
88
- function ensureStageExecutionCostColumns(db) {
89
- const cols = db
90
- .prepare(`PRAGMA table_info(stage_executions)`)
91
- .all();
92
- const names = new Set(cols.map((c) => c.name));
93
- if (!names.has("cost_usd")) {
94
- db.exec(`ALTER TABLE stage_executions ADD COLUMN cost_usd REAL`);
95
- }
96
- if (!names.has("usage_json")) {
97
- db.exec(`ALTER TABLE stage_executions ADD COLUMN usage_json TEXT`);
98
- }
99
- }
100
- function backfillVerificationOutcomes(db) {
101
- db.exec(`
102
- UPDATE stage_executions
103
- SET verification_outcome = CASE
104
- WHEN status = 'succeeded'
105
- AND EXISTS (
106
- SELECT 1 FROM verification_check_results AS check_result
107
- WHERE check_result.run_id = stage_executions.run_id
108
- AND check_result.stage_id = stage_executions.stage_id
109
- AND check_result.attempt = stage_executions.attempt
110
- )
111
- AND NOT EXISTS (
112
- SELECT 1 FROM verification_check_results AS check_result
113
- WHERE check_result.run_id = stage_executions.run_id
114
- AND check_result.stage_id = stage_executions.stage_id
115
- AND check_result.attempt = stage_executions.attempt
116
- AND check_result.status != 'passed'
117
- )
118
- THEN 'passed'
119
- WHEN status = 'failed'
120
- AND EXISTS (
121
- SELECT 1 FROM verification_check_results AS check_result
122
- WHERE check_result.run_id = stage_executions.run_id
123
- AND check_result.stage_id = stage_executions.stage_id
124
- AND check_result.attempt = stage_executions.attempt
125
- )
126
- THEN 'failed'
127
- ELSE verification_outcome
128
- END
129
- WHERE verification_outcome = 'not_run';
130
- `);
131
- }
132
- function ensureStageEventsAttemptColumn(db) {
133
- const cols = db
134
- .prepare(`PRAGMA table_info(stage_events)`)
135
- .all();
136
- if (!cols.some((c) => c.name === "attempt")) {
137
- db.exec(`ALTER TABLE stage_events ADD COLUMN attempt INTEGER NOT NULL DEFAULT 1`);
138
- db.exec(`
139
- CREATE INDEX IF NOT EXISTS idx_stage_events_run_stage_attempt_at
140
- ON stage_events (run_id, stage_id, attempt, at);
141
- `);
142
- }
143
- }
144
- function ensureVerificationCheckResultsTable(db) {
145
- db.exec(`
146
- CREATE TABLE IF NOT EXISTS verification_check_results (
147
- run_id TEXT NOT NULL,
148
- stage_id TEXT NOT NULL,
149
- attempt INTEGER NOT NULL,
150
- check_id TEXT NOT NULL,
151
- check_type TEXT NOT NULL,
152
- status TEXT NOT NULL,
153
- started_at TEXT,
154
- finished_at TEXT,
155
- evidence_json TEXT,
156
- PRIMARY KEY (run_id, stage_id, attempt, check_id),
157
- FOREIGN KEY (run_id, stage_id, attempt)
158
- REFERENCES stage_executions(run_id, stage_id, attempt)
159
- );
160
- CREATE INDEX IF NOT EXISTS idx_verification_check_results_execution
161
- ON verification_check_results (run_id, stage_id, attempt, check_id);
162
- `);
163
- }
164
- function ensureFeedbackReplayStagePassEnvelopeColumn(db) {
165
- const cols = db
166
- .prepare(`PRAGMA table_info(feedback_replay_stage_passes)`)
167
- .all();
168
- if (cols.length === 0)
169
- return;
170
- if (!cols.some((c) => c.name === "emitted_envelope_json")) {
171
- db.exec(`ALTER TABLE feedback_replay_stage_passes ADD COLUMN emitted_envelope_json TEXT`);
172
- }
173
- }
174
- function ensureFeedbackReplayStagePassSessionOriginColumn(db) {
175
- const cols = db
176
- .prepare(`PRAGMA table_info(feedback_replay_stage_passes)`)
177
- .all();
178
- if (cols.length === 0)
179
- return;
180
- if (!cols.some((c) => c.name === "session_origin_attempt")) {
181
- db.exec(`ALTER TABLE feedback_replay_stage_passes ADD COLUMN session_origin_attempt INTEGER`);
182
- }
183
- }
184
- function ensureFeedbackLoopDeferredSendBackColumn(db) {
185
- const cols = db
186
- .prepare(`PRAGMA table_info(feedback_loops)`)
187
- .all();
188
- if (cols.length === 0)
189
- return;
190
- if (!cols.some((c) => c.name === "deferred_send_back_json")) {
191
- db.exec(`ALTER TABLE feedback_loops ADD COLUMN deferred_send_back_json TEXT`);
192
- }
193
- }
194
18
  function executionFromRow(row) {
195
19
  return {
196
20
  run_id: row.run_id,
@@ -204,7 +28,10 @@ function executionFromRow(row) {
204
28
  ? JSON.parse(row.envelope_json)
205
29
  : null,
206
30
  ...(row.cost_usd != null ? { cost_usd: row.cost_usd } : {}),
207
- ...(row.usage_json != null ? { usage: JSON.parse(row.usage_json) } : {}),
31
+ ...(row.usage_json != null
32
+ ? { usage: JSON.parse(row.usage_json) }
33
+ : {}),
34
+ auto_resume_count: row.auto_resume_count ?? 0,
208
35
  };
209
36
  }
210
37
  function verificationCheckResultFromRow(row) {
@@ -300,33 +127,44 @@ export class SqliteRunStore {
300
127
  storeRoot;
301
128
  db;
302
129
  migratePromise;
303
- constructor(storeRoot) {
130
+ constructor(storeRoot, options) {
304
131
  this.storeRoot = storeRoot;
132
+ const openerMode = options?.openerMode ??
133
+ (process.env.VITEST === "true" ? "migrate" : "assert");
305
134
  const dbPath = path.join(storeRoot, "state.db");
135
+ if (openerMode === "assert") {
136
+ if (!existsSync(dbPath)) {
137
+ throw new StoreSchemaError("store_schema_migration_required: database file is missing", "store_schema_migration_required");
138
+ }
139
+ this.db = new Database(dbPath);
140
+ applyStorePragmas(this.db);
141
+ assertSchemaVersion(this.db);
142
+ this.migratePromise = Promise.resolve();
143
+ return;
144
+ }
306
145
  this.db = new Database(dbPath);
307
- this.db.pragma("journal_mode = WAL");
308
- this.db.pragma(`busy_timeout = ${readSqliteBusyTimeoutMs()}`);
309
- this.db.exec(SCHEMA_SQL);
310
- ensureCheckoutRootColumn(this.db);
311
- ensureCiIdentityColumns(this.db);
312
- ensurePipelineDagColumn(this.db);
313
- ensureRunLocatorColumns(this.db);
314
- ensureInlinePipelineColumn(this.db);
315
- ensureStageExecutionsTable(this.db);
316
- ensureStageExecutionVerificationOutcomeColumn(this.db);
317
- ensureStageExecutionCostColumns(this.db);
318
- ensureStageEventsAttemptColumn(this.db);
319
- ensureVerificationCheckResultsTable(this.db);
320
- ensureFeedbackReplayStagePassEnvelopeColumn(this.db);
321
- ensureFeedbackReplayStagePassSessionOriginColumn(this.db);
322
- ensureFeedbackLoopDeferredSendBackColumn(this.db);
323
- backfillVerificationOutcomes(this.db);
146
+ applyStorePragmas(this.db);
147
+ applyPendingMigrations(this.db);
324
148
  this.migratePromise = importDiskRunsIfEmpty(this.db, storeRoot).then(() => undefined);
325
149
  }
326
150
  /** Await one-shot disk import before catalog reads (create paths also wait). */
327
151
  async ready() {
328
152
  await this.migratePromise;
329
153
  }
154
+ async close() {
155
+ await this.ready();
156
+ this.db.pragma("wal_checkpoint(TRUNCATE)");
157
+ this.db.close();
158
+ }
159
+ async snapshotInto(destPath) {
160
+ await this.ready();
161
+ const userVersion = this.db.pragma("user_version", {
162
+ simple: true,
163
+ });
164
+ const escaped = destPath.replace(/'/g, "''");
165
+ this.db.exec(`VACUUM INTO '${escaped}'`);
166
+ return { userVersion };
167
+ }
330
168
  /**
331
169
  * The connection this store owns, for the one caller allowed to share it: the composition root
332
170
  * wiring the A2A tables into the same `state.db` file. Not part of the `RunStore` interface.
@@ -339,7 +177,7 @@ export class SqliteRunStore {
339
177
  }
340
178
  async createRun(input) {
341
179
  await this.ready();
342
- const runId = newRunId();
180
+ const runId = input.runId ?? newRunId();
343
181
  const workspaceDir = this.getWorkspaceDir(runId);
344
182
  await mkdir(path.join(workspaceDir, "stages"), { recursive: true });
345
183
  const now = new Date().toISOString();
@@ -352,6 +190,12 @@ export class SqliteRunStore {
352
190
  const projectRoot = input.projectRoot
353
191
  ? normalizeCatalogPath(input.projectRoot)
354
192
  : null;
193
+ const pipelineBody = input.pipelineBody ??
194
+ (input.inlinePipeline !== undefined
195
+ ? JSON.stringify(input.inlinePipeline)
196
+ : null);
197
+ const pipelineSource = input.pipelineSource ??
198
+ (pipelineBody != null && pipelinePath == null ? "inline" : null);
355
199
  this.db.transaction(() => {
356
200
  if (input.submission) {
357
201
  const existing = this.readSubmission(input.submission.key);
@@ -360,15 +204,15 @@ export class SqliteRunStore {
360
204
  }
361
205
  this.db
362
206
  .prepare(`INSERT INTO runs
363
- (run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json)
207
+ (run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, repository, ref, resolved_sha, run_branch, git_author_name, git_author_email, pipeline_source, pipeline_body, caller_id, run_manifest, skip_gates)
364
208
  VALUES
365
- (@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root, @inline_pipeline_json)`)
209
+ (@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root, @repository, @ref, @resolved_sha, @run_branch, @git_author_name, @git_author_email, @pipeline_source, @pipeline_body, @caller_id, @run_manifest, @skip_gates)`)
366
210
  .run({
367
211
  run_id: runId,
368
212
  pipeline_id: input.pipelineId,
369
213
  task_id: input.taskId ?? null,
370
214
  task_yaml: input.taskYaml,
371
- status: "running",
215
+ status: input.status ?? "running",
372
216
  created_at: now,
373
217
  updated_at: now,
374
218
  checkout_root: input.checkoutRoot ?? null,
@@ -381,9 +225,19 @@ export class SqliteRunStore {
381
225
  pipeline_path: pipelinePath,
382
226
  task_path: taskPath,
383
227
  project_root: projectRoot,
384
- inline_pipeline_json: input.inlinePipeline
385
- ? JSON.stringify(input.inlinePipeline)
228
+ repository: input.repository ?? null,
229
+ ref: input.ref ?? null,
230
+ resolved_sha: input.resolvedSha ?? null,
231
+ run_branch: input.runBranch ?? null,
232
+ git_author_name: input.gitAuthorName ?? null,
233
+ git_author_email: input.gitAuthorEmail ?? null,
234
+ pipeline_source: pipelineSource,
235
+ pipeline_body: pipelineBody,
236
+ caller_id: input.callerId ?? null,
237
+ run_manifest: input.runManifest !== undefined
238
+ ? JSON.stringify(input.runManifest)
386
239
  : null,
240
+ skip_gates: input.skipGates === undefined ? null : input.skipGates ? 1 : 0,
387
241
  });
388
242
  if (input.submission) {
389
243
  this.db.prepare("INSERT INTO run_submissions (submission_key, request_hash, run_id) VALUES (?, ?, ?)")
@@ -403,17 +257,198 @@ export class SqliteRunStore {
403
257
  }
404
258
  async updateRunStatus(runId, status) {
405
259
  await this.ready();
260
+ const now = new Date().toISOString();
261
+ const isTerminal = status === "succeeded" || status === "failed" || status === "cancelled";
262
+ const result = isTerminal
263
+ ? this.db
264
+ .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at,
265
+ finished_at = COALESCE(finished_at, @finished_at)
266
+ WHERE run_id = @run_id`)
267
+ .run({
268
+ run_id: runId,
269
+ status,
270
+ updated_at: now,
271
+ finished_at: now,
272
+ })
273
+ : this.db
274
+ .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at WHERE run_id = @run_id`)
275
+ .run({
276
+ run_id: runId,
277
+ status,
278
+ updated_at: now,
279
+ });
280
+ if (result.changes === 0) {
281
+ throw new Error(`Run not found: ${runId}`);
282
+ }
283
+ }
284
+ async tryUpdateRunStatus(runId, status, expectedStatus) {
285
+ await this.ready();
286
+ const now = new Date().toISOString();
287
+ const isTerminal = status === "succeeded" || status === "failed" || status === "cancelled";
288
+ const result = isTerminal
289
+ ? this.db
290
+ .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at,
291
+ finished_at = COALESCE(finished_at, @finished_at)
292
+ WHERE run_id = @run_id AND status = @expected_status`)
293
+ .run({
294
+ run_id: runId,
295
+ status,
296
+ expected_status: expectedStatus,
297
+ updated_at: now,
298
+ finished_at: now,
299
+ })
300
+ : this.db
301
+ .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at
302
+ WHERE run_id = @run_id AND status = @expected_status`)
303
+ .run({
304
+ run_id: runId,
305
+ status,
306
+ expected_status: expectedStatus,
307
+ updated_at: now,
308
+ });
309
+ if (result.changes > 0)
310
+ return true;
311
+ const exists = this.db
312
+ .prepare(`SELECT 1 AS ok FROM runs WHERE run_id = ?`)
313
+ .get(runId);
314
+ if (exists === undefined) {
315
+ throw new Error(`Run not found: ${runId}`);
316
+ }
317
+ return false;
318
+ }
319
+ async patchRunWorkspaceBinding(runId, patch) {
320
+ await this.ready();
321
+ const sets = ["updated_at = @updated_at"];
322
+ const params = {
323
+ run_id: runId,
324
+ updated_at: new Date().toISOString(),
325
+ };
326
+ if (patch.checkoutRoot !== undefined) {
327
+ sets.push("checkout_root = @checkout_root");
328
+ params.checkout_root = patch.checkoutRoot;
329
+ }
330
+ if (patch.repository !== undefined) {
331
+ sets.push("repository = @repository");
332
+ params.repository = patch.repository;
333
+ }
334
+ if (patch.ref !== undefined) {
335
+ sets.push("ref = @ref");
336
+ params.ref = patch.ref;
337
+ }
338
+ if (patch.resolvedSha !== undefined) {
339
+ sets.push("resolved_sha = @resolved_sha");
340
+ params.resolved_sha = patch.resolvedSha;
341
+ }
342
+ if (patch.runBranch !== undefined) {
343
+ sets.push("run_branch = @run_branch");
344
+ params.run_branch = patch.runBranch;
345
+ }
346
+ if (sets.length === 1) {
347
+ return;
348
+ }
406
349
  const result = this.db
407
- .prepare(`UPDATE runs SET status = @status, updated_at = @updated_at WHERE run_id = @run_id`)
350
+ .prepare(`UPDATE runs SET ${sets.join(", ")} WHERE run_id = @run_id`)
351
+ .run(params);
352
+ if (result.changes === 0) {
353
+ throw new Error(`Run not found: ${runId}`);
354
+ }
355
+ }
356
+ async appendConfigOrigins(runId, origins) {
357
+ await this.ready();
358
+ if (origins.length === 0)
359
+ return;
360
+ const row = this.getRunRow(runId);
361
+ const existing = row.config_origins_json
362
+ ? JSON.parse(row.config_origins_json)
363
+ : [];
364
+ const keyOf = (o) => `${o.name}\0${o.origin}\0${o.path ?? ""}`;
365
+ const seen = new Set(existing.map(keyOf));
366
+ const merged = [...existing];
367
+ for (const origin of origins) {
368
+ const key = keyOf(origin);
369
+ if (seen.has(key))
370
+ continue;
371
+ seen.add(key);
372
+ merged.push(origin);
373
+ }
374
+ if (merged.length === existing.length)
375
+ return;
376
+ const result = this.db
377
+ .prepare(`UPDATE runs SET config_origins_json = @config_origins_json, updated_at = @updated_at WHERE run_id = @run_id`)
408
378
  .run({
409
379
  run_id: runId,
410
- status,
380
+ config_origins_json: JSON.stringify(merged),
411
381
  updated_at: new Date().toISOString(),
412
382
  });
413
383
  if (result.changes === 0) {
414
384
  throw new Error(`Run not found: ${runId}`);
415
385
  }
416
386
  }
387
+ async setCancelReason(runId, reason) {
388
+ await this.ready();
389
+ const result = this.db
390
+ .prepare(`UPDATE runs SET cancel_reason = @cancel_reason, updated_at = @updated_at WHERE run_id = @run_id`)
391
+ .run({
392
+ run_id: runId,
393
+ cancel_reason: reason,
394
+ updated_at: new Date().toISOString(),
395
+ });
396
+ if (result.changes === 0) {
397
+ throw new Error(`Run not found: ${runId}`);
398
+ }
399
+ }
400
+ async setRunDiskUsage(runId, diskBytes, measuredAt) {
401
+ await this.ready();
402
+ const result = this.db
403
+ .prepare(`UPDATE runs SET disk_bytes = @disk_bytes, disk_measured_at = @disk_measured_at
404
+ WHERE run_id = @run_id`)
405
+ .run({
406
+ run_id: runId,
407
+ disk_bytes: diskBytes,
408
+ disk_measured_at: measuredAt,
409
+ });
410
+ if (result.changes === 0) {
411
+ throw new Error(`Run not found: ${runId}`);
412
+ }
413
+ }
414
+ async setSlimmedAt(runId, slimmedAt) {
415
+ await this.ready();
416
+ const result = this.db
417
+ .prepare(`UPDATE runs SET slimmed_at = @slimmed_at WHERE run_id = @run_id`)
418
+ .run({
419
+ run_id: runId,
420
+ slimmed_at: slimmedAt,
421
+ });
422
+ if (result.changes === 0) {
423
+ throw new Error(`Run not found: ${runId}`);
424
+ }
425
+ }
426
+ async deleteRun(runId) {
427
+ await this.ready();
428
+ const deleteAll = this.db.transaction(() => {
429
+ const exists = this.db
430
+ .prepare(`SELECT 1 AS ok FROM runs WHERE run_id = ?`)
431
+ .get(runId);
432
+ if (exists === undefined) {
433
+ throw new Error(`Run not found: ${runId}`);
434
+ }
435
+ this.db.prepare(`DELETE FROM stage_events WHERE run_id = ?`).run(runId);
436
+ this.db
437
+ .prepare(`DELETE FROM verification_check_results WHERE run_id = ?`)
438
+ .run(runId);
439
+ this.db.prepare(`DELETE FROM stage_executions WHERE run_id = ?`).run(runId);
440
+ this.db
441
+ .prepare(`DELETE FROM feedback_replay_stage_passes WHERE run_id = ?`)
442
+ .run(runId);
443
+ this.db.prepare(`DELETE FROM fork_generations WHERE run_id = ?`).run(runId);
444
+ this.db.prepare(`DELETE FROM feedback_replays WHERE run_id = ?`).run(runId);
445
+ this.db.prepare(`DELETE FROM feedback_loops WHERE run_id = ?`).run(runId);
446
+ this.db.prepare(`DELETE FROM stages WHERE run_id = ?`).run(runId);
447
+ this.db.prepare(`DELETE FROM run_submissions WHERE run_id = ?`).run(runId);
448
+ this.db.prepare(`DELETE FROM runs WHERE run_id = ?`).run(runId);
449
+ });
450
+ deleteAll();
451
+ }
417
452
  async updatePipelineDag(runId, dag) {
418
453
  await this.ready();
419
454
  const result = this.db
@@ -435,6 +470,25 @@ export class SqliteRunStore {
435
470
  await this.ready();
436
471
  return this.getRunRow(runId).task_yaml;
437
472
  }
473
+ async readPipelineBody(runId) {
474
+ await this.ready();
475
+ return this.getRunRow(runId).pipeline_body;
476
+ }
477
+ async updateRunManifest(runId, manifest) {
478
+ await this.ready();
479
+ const result = this.db
480
+ .prepare(`UPDATE runs SET run_manifest = @run_manifest, updated_at = @updated_at WHERE run_id = @run_id`)
481
+ .run({
482
+ run_id: runId,
483
+ run_manifest: manifest === null || manifest === undefined
484
+ ? null
485
+ : JSON.stringify(manifest),
486
+ updated_at: new Date().toISOString(),
487
+ });
488
+ if (result.changes === 0) {
489
+ throw new Error(`Run not found: ${runId}`);
490
+ }
491
+ }
438
492
  async ensureStageWorkspace(runId, stageId) {
439
493
  await this.ready();
440
494
  this.getRunRow(runId);
@@ -485,7 +539,7 @@ export class SqliteRunStore {
485
539
  verification_outcome: "not_run",
486
540
  });
487
541
  const row = this.db
488
- .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json, cost_usd, usage_json
542
+ .prepare(`SELECT ${EXECUTION_SELECT_COLS}
489
543
  FROM stage_executions
490
544
  WHERE run_id = ? AND stage_id = ? AND attempt = ?`)
491
545
  .get(runId, stageId, attempt);
@@ -495,7 +549,7 @@ export class SqliteRunStore {
495
549
  async listStageExecutions(runId, stageId) {
496
550
  await this.ready();
497
551
  const rows = this.db
498
- .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json, cost_usd, usage_json
552
+ .prepare(`SELECT ${EXECUTION_SELECT_COLS}
499
553
  FROM stage_executions
500
554
  WHERE run_id = ? AND stage_id = ?
501
555
  ORDER BY attempt ASC`)
@@ -505,7 +559,7 @@ export class SqliteRunStore {
505
559
  async getLatestStageExecution(runId, stageId) {
506
560
  await this.ready();
507
561
  const row = this.db
508
- .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json, cost_usd, usage_json
562
+ .prepare(`SELECT ${EXECUTION_SELECT_COLS}
509
563
  FROM stage_executions
510
564
  WHERE run_id = ? AND stage_id = ?
511
565
  ORDER BY attempt DESC
@@ -524,7 +578,7 @@ export class SqliteRunStore {
524
578
  async getStageExecution(runId, stageId, attempt) {
525
579
  await this.ready();
526
580
  const row = this.db
527
- .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json, cost_usd, usage_json
581
+ .prepare(`SELECT ${EXECUTION_SELECT_COLS}
528
582
  FROM stage_executions
529
583
  WHERE run_id = ? AND stage_id = ? AND attempt = ?`)
530
584
  .get(runId, stageId, attempt);
@@ -533,6 +587,21 @@ export class SqliteRunStore {
533
587
  }
534
588
  return executionFromRow(row);
535
589
  }
590
+ async listInterruptedStageExecutions() {
591
+ await this.ready();
592
+ const rows = this.db
593
+ .prepare(`SELECT ${EXECUTION_SELECT_COLS}
594
+ FROM stage_executions e
595
+ WHERE e.status = 'interrupted'
596
+ AND e.attempt = (
597
+ SELECT MAX(e2.attempt)
598
+ FROM stage_executions e2
599
+ WHERE e2.run_id = e.run_id AND e2.stage_id = e.stage_id
600
+ )
601
+ ORDER BY e.run_id ASC, e.stage_id ASC, e.attempt ASC`)
602
+ .all();
603
+ return rows.map(executionFromRow);
604
+ }
536
605
  async updateStageExecution(runId, stageId, attempt, patch) {
537
606
  await this.ready();
538
607
  const sets = [];
@@ -570,6 +639,10 @@ export class SqliteRunStore {
570
639
  sets.push("usage_json = @usage_json");
571
640
  params.usage_json = JSON.stringify(patch.usage);
572
641
  }
642
+ if (patch.auto_resume_count !== undefined) {
643
+ sets.push("auto_resume_count = @auto_resume_count");
644
+ params.auto_resume_count = patch.auto_resume_count;
645
+ }
573
646
  if (sets.length === 0)
574
647
  return;
575
648
  const result = this.db
@@ -746,9 +819,13 @@ export class SqliteRunStore {
746
819
  clauses.push("(pipeline_id = ? OR pipeline_path = ? OR pipeline_path = ?)");
747
820
  params.push(pipeline, pipeline, normalized);
748
821
  }
822
+ if (filter?.caller_id !== undefined) {
823
+ clauses.push("caller_id = ?");
824
+ params.push(filter.caller_id);
825
+ }
749
826
  const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
750
827
  const rows = this.db
751
- .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json
828
+ .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, repository, ref, resolved_sha, run_branch, git_author_name, git_author_email, cancel_reason, finished_at, slimmed_at, disk_bytes, disk_measured_at, config_origins_json, pipeline_source, pipeline_body, caller_id, run_manifest, skip_gates
752
829
  FROM runs ${where} ORDER BY created_at DESC`)
753
830
  .all(...params);
754
831
  const summaries = [];
@@ -762,10 +839,34 @@ export class SqliteRunStore {
762
839
  }
763
840
  return summaries;
764
841
  }
765
- async listProjectRoots() {
842
+ async ensureProject(absPath) {
843
+ await this.ready();
844
+ const resolved = path.resolve(absPath);
845
+ let st;
846
+ try {
847
+ st = statSync(resolved);
848
+ }
849
+ catch {
850
+ throw new Error(`project_root does not exist: ${resolved}`);
851
+ }
852
+ if (!st.isDirectory()) {
853
+ throw new Error(`project_root is not a directory: ${resolved}`);
854
+ }
855
+ const normalized = normalizeProjectRoot(absPath);
856
+ if (normalized === path.parse(normalized).root) {
857
+ throw new Error(`project_root must not be filesystem root: ${normalized}`);
858
+ }
859
+ const now = new Date().toISOString();
860
+ this.db
861
+ .prepare(`INSERT INTO projects (project_root, created_at) VALUES (?, ?)
862
+ ON CONFLICT(project_root) DO NOTHING`)
863
+ .run(normalized, now);
864
+ return normalized;
865
+ }
866
+ async listRegisteredProjects() {
766
867
  await this.ready();
767
868
  const rows = this.db
768
- .prepare(`SELECT DISTINCT project_root FROM runs WHERE project_root IS NOT NULL AND project_root != ''`)
869
+ .prepare(`SELECT project_root FROM projects ORDER BY project_root`)
769
870
  .all();
770
871
  return rows.map((row) => row.project_root);
771
872
  }
@@ -1176,15 +1277,47 @@ export class SqliteRunStore {
1176
1277
  ...(row.pipeline_path != null ? { pipeline_path: row.pipeline_path } : {}),
1177
1278
  ...(row.task_path != null ? { task_path: row.task_path } : {}),
1178
1279
  ...(row.project_root != null ? { project_root: row.project_root } : {}),
1280
+ ...(row.repository != null ? { repository: row.repository } : {}),
1281
+ ...(row.ref != null ? { ref: row.ref } : {}),
1282
+ ...(row.resolved_sha != null ? { resolved_sha: row.resolved_sha } : {}),
1283
+ ...(row.run_branch != null ? { run_branch: row.run_branch } : {}),
1284
+ ...(row.git_author_name != null
1285
+ ? { git_author_name: row.git_author_name }
1286
+ : {}),
1287
+ ...(row.git_author_email != null
1288
+ ? { git_author_email: row.git_author_email }
1289
+ : {}),
1290
+ ...(row.cancel_reason != null ? { cancel_reason: row.cancel_reason } : {}),
1291
+ ...(row.finished_at != null ? { finished_at: row.finished_at } : {}),
1292
+ ...(row.slimmed_at != null ? { slimmed_at: row.slimmed_at } : {}),
1293
+ ...(row.disk_bytes != null ? { disk_bytes: row.disk_bytes } : {}),
1294
+ ...(row.disk_measured_at != null
1295
+ ? { disk_measured_at: row.disk_measured_at }
1296
+ : {}),
1297
+ ...(row.config_origins_json
1298
+ ? {
1299
+ config_origins: JSON.parse(row.config_origins_json),
1300
+ }
1301
+ : {}),
1302
+ ...(row.pipeline_source === "inline" || row.pipeline_source === "path"
1303
+ ? { pipeline_source: row.pipeline_source }
1304
+ : {}),
1305
+ ...(row.caller_id != null ? { caller_id: row.caller_id } : {}),
1306
+ ...(row.run_manifest
1307
+ ? { run_manifest: JSON.parse(row.run_manifest) }
1308
+ : {}),
1309
+ ...(row.skip_gates != null ? { skip_gates: row.skip_gates !== 0 } : {}),
1179
1310
  ...(pipeline_dag ? { pipeline_dag } : {}),
1180
- ...(row.inline_pipeline_json != null
1181
- ? { inline_pipeline: JSON.parse(row.inline_pipeline_json) }
1311
+ ...(row.pipeline_body
1312
+ ? {
1313
+ inline_pipeline: JSON.parse(row.pipeline_body),
1314
+ }
1182
1315
  : {}),
1183
1316
  };
1184
1317
  }
1185
1318
  getRunRow(runId) {
1186
1319
  const row = this.db
1187
- .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json
1320
+ .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, repository, ref, resolved_sha, run_branch, git_author_name, git_author_email, cancel_reason, finished_at, slimmed_at, disk_bytes, disk_measured_at, config_origins_json, pipeline_source, pipeline_body, caller_id, run_manifest, skip_gates
1188
1321
  FROM runs WHERE run_id = ?`)
1189
1322
  .get(runId);
1190
1323
  if (!row)