stageflow 0.32.0 → 0.35.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 (335) hide show
  1. package/README.md +4 -3
  2. package/dist/agent/adapterSpill.d.ts +14 -0
  3. package/dist/agent/adapterSpill.js +83 -0
  4. package/dist/agent/agentBackend.d.ts +2 -2
  5. package/dist/agent/agentBackend.js +2 -2
  6. package/dist/agent/claudeAdapter.d.ts +13 -2
  7. package/dist/agent/claudeAdapter.js +50 -6
  8. package/dist/agent/cursorProvider.d.ts +3 -0
  9. package/dist/agent/cursorProvider.js +23 -6
  10. package/dist/agent/fakeAgent.d.ts +3 -3
  11. package/dist/agent/fakeAgent.js +0 -6
  12. package/dist/agent/piAdapter.d.ts +2 -2
  13. package/dist/agent/piAdapter.js +116 -15
  14. package/dist/agent/piIsolatedMcp.d.ts +1 -0
  15. package/dist/agent/piIsolatedMcp.js +19 -1
  16. package/dist/agent/piIsolatedMcpProbe.d.ts +11 -0
  17. package/dist/agent/piIsolatedMcpProbe.js +20 -0
  18. package/dist/agent/port.d.ts +2 -2
  19. package/dist/agent/providerAuth.d.ts +6 -0
  20. package/dist/agent/providerAuth.js +86 -17
  21. package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -6
  22. package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +13 -9
  23. package/dist/agent/stageTurnDriver.d.ts +33 -0
  24. package/dist/agent/stageTurnDriver.js +64 -0
  25. package/dist/agent/toolOutputBudget.d.ts +46 -0
  26. package/dist/agent/toolOutputBudget.js +97 -0
  27. package/dist/agent/toolOutputBudgetExtension.d.ts +22 -0
  28. package/dist/agent/toolOutputBudgetExtension.js +141 -0
  29. package/dist/agent/toolOutputSpill.d.ts +34 -0
  30. package/dist/agent/toolOutputSpill.js +85 -0
  31. package/dist/browser/agentBrowserLiveViewRelay.d.ts +41 -0
  32. package/dist/browser/agentBrowserLiveViewRelay.js +801 -0
  33. package/dist/browser/anchor.d.ts +2 -0
  34. package/dist/browser/anchor.js +2 -1
  35. package/dist/browser/auditSink.d.ts +8 -0
  36. package/dist/browser/browserHost.d.ts +29 -0
  37. package/dist/browser/browserSweep.d.ts +4 -1
  38. package/dist/browser/browserSweep.js +7 -0
  39. package/dist/browser/browserTeardown.d.ts +1 -0
  40. package/dist/browser/browserTeardown.js +32 -2
  41. package/dist/browser/containerBrowserEndpoint.d.ts +11 -0
  42. package/dist/browser/containerBrowserEndpoint.js +57 -0
  43. package/dist/browser/containerBrowserHost.d.ts +24 -0
  44. package/dist/browser/containerBrowserHost.js +147 -0
  45. package/dist/browser/dockerSandboxOrchestrator.d.ts +26 -0
  46. package/dist/browser/dockerSandboxOrchestrator.js +142 -0
  47. package/dist/browser/fakeLiveViewRelay.d.ts +15 -0
  48. package/dist/browser/fakeLiveViewRelay.js +94 -0
  49. package/dist/browser/fakeLiveViewSource.d.ts +6 -0
  50. package/dist/browser/fakeLiveViewSource.js +16 -0
  51. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -1
  52. package/dist/browser/fakeRemoteBrowserHost.js +2 -1
  53. package/dist/browser/fakeSandboxOrchestrator.d.ts +9 -0
  54. package/dist/browser/fakeSandboxOrchestrator.js +44 -0
  55. package/dist/browser/gateHandoff.d.ts +0 -2
  56. package/dist/browser/gateHandoff.js +5 -11
  57. package/dist/browser/hostCapabilities.d.ts +23 -0
  58. package/dist/browser/hostCapabilities.js +23 -0
  59. package/dist/browser/humanLogin.d.ts +2 -2
  60. package/dist/browser/humanLogin.js +11 -2
  61. package/dist/browser/liveViewPersisted.d.ts +11 -0
  62. package/dist/browser/liveViewPersisted.js +22 -0
  63. package/dist/browser/liveViewRelay.d.ts +76 -0
  64. package/dist/browser/liveViewRelay.js +1 -0
  65. package/dist/browser/liveViewSessions.d.ts +25 -0
  66. package/dist/browser/liveViewSessions.js +139 -0
  67. package/dist/browser/liveViewSource.d.ts +25 -0
  68. package/dist/browser/liveViewSource.js +7 -0
  69. package/dist/browser/liveViewTickets.d.ts +46 -0
  70. package/dist/browser/liveViewTickets.js +122 -0
  71. package/dist/browser/localBrowserHost.d.ts +10 -0
  72. package/dist/browser/localBrowserHost.js +53 -7
  73. package/dist/browser/ownerScope.d.ts +10 -0
  74. package/dist/browser/ownerScope.js +6 -0
  75. package/dist/browser/profileStore.d.ts +5 -2
  76. package/dist/browser/profileStore.js +3 -5
  77. package/dist/browser/sandboxOrchestrator.d.ts +60 -0
  78. package/dist/browser/sandboxOrchestrator.js +8 -0
  79. package/dist/browser/sessionApiSandboxOrchestrator.d.ts +34 -0
  80. package/dist/browser/sessionApiSandboxOrchestrator.js +108 -0
  81. package/dist/browser/stageBrowserEnv.d.ts +7 -0
  82. package/dist/browser/stageBrowserEnv.js +53 -15
  83. package/dist/browser/stageHandoff.d.ts +47 -0
  84. package/dist/browser/stageHandoff.js +101 -0
  85. package/dist/browser/stageProfileLock.d.ts +1 -0
  86. package/dist/browser/stageProfileLock.js +1 -3
  87. package/dist/browser/volumeProfileStore.d.ts +7 -0
  88. package/dist/browser/volumeProfileStore.js +35 -0
  89. package/dist/cli/browserCommand.d.ts +3 -1
  90. package/dist/cli/browserCommand.js +25 -20
  91. package/dist/cli/resolveCliProjectRoot.d.ts +7 -0
  92. package/dist/cli/resolveCliProjectRoot.js +27 -0
  93. package/dist/cli/runCommand.d.ts +1 -1
  94. package/dist/cli/runCommand.js +59 -17
  95. package/dist/cli/skillsCommand.d.ts +20 -0
  96. package/dist/cli/skillsCommand.js +74 -31
  97. package/dist/cli/validateCommand.d.ts +1 -1
  98. package/dist/cli/validateCommand.js +22 -4
  99. package/dist/cli.js +4 -4
  100. package/dist/config/browseCatalog.d.ts +2 -0
  101. package/dist/config/browseCatalog.js +6 -0
  102. package/dist/config/catalogRelativePath.d.ts +4 -0
  103. package/dist/config/catalogRelativePath.js +37 -9
  104. package/dist/config/createStage.d.ts +1 -0
  105. package/dist/config/createStage.js +11 -2
  106. package/dist/config/createTask.d.ts +30 -0
  107. package/dist/config/createTask.js +161 -0
  108. package/dist/config/createTrigger.d.ts +15 -0
  109. package/dist/config/createTrigger.js +32 -25
  110. package/dist/config/draftPackage.d.ts +41 -0
  111. package/dist/config/draftPackage.js +216 -23
  112. package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
  113. package/dist/config/ensureCatalogScanRoot.js +6 -2
  114. package/dist/config/findTaskInCatalog.d.ts +33 -0
  115. package/dist/config/findTaskInCatalog.js +69 -0
  116. package/dist/config/hostConfig.d.ts +21 -0
  117. package/dist/config/hostConfig.js +113 -1
  118. package/dist/config/importTask.d.ts +19 -0
  119. package/dist/config/importTask.js +133 -0
  120. package/dist/config/installSkillRequest.d.ts +40 -0
  121. package/dist/config/installSkillRequest.js +286 -0
  122. package/dist/config/listConfig.d.ts +0 -16
  123. package/dist/config/listConfig.js +0 -3
  124. package/dist/config/loadOutcome.d.ts +11 -6
  125. package/dist/config/loadPipeline.js +0 -1
  126. package/dist/config/loadStage.js +2 -0
  127. package/dist/config/loadStageflowManifest.js +4 -1
  128. package/dist/config/loadTask.js +2 -0
  129. package/dist/config/loadTrigger.js +2 -0
  130. package/dist/config/mergePipelineIncludes.js +3 -0
  131. package/dist/config/normalizePipelineStageEntry.js +10 -1
  132. package/dist/config/printTargetYaml.js +0 -1
  133. package/dist/config/publishDraftPackage.d.ts +5 -1
  134. package/dist/config/publishDraftPackage.js +4 -1
  135. package/dist/config/resolveCatalogRoots.d.ts +4 -1
  136. package/dist/config/resolveCatalogRoots.js +18 -1
  137. package/dist/config/skillUsageIndex.d.ts +6 -0
  138. package/dist/config/skillUsageIndex.js +39 -0
  139. package/dist/config/stageBrowser.js +13 -0
  140. package/dist/config/triggerAdapterStatus.d.ts +11 -0
  141. package/dist/config/triggerAdapterStatus.js +90 -0
  142. package/dist/config/triggerCatalog.d.ts +41 -2
  143. package/dist/config/triggerCatalog.js +107 -15
  144. package/dist/config/updateTask.d.ts +30 -0
  145. package/dist/config/updateTask.js +119 -0
  146. package/dist/config/updateTrigger.d.ts +38 -0
  147. package/dist/config/updateTrigger.js +220 -0
  148. package/dist/config/validateCatalog.d.ts +6 -2
  149. package/dist/config/validateCatalog.js +48 -7
  150. package/dist/config/workshopCatalogFilenames.d.ts +37 -0
  151. package/dist/config/workshopCatalogFilenames.js +125 -0
  152. package/dist/config/workshopCatalogRepair.d.ts +7 -0
  153. package/dist/config/workshopCatalogRepair.js +144 -0
  154. package/dist/config/yamlParsePosition.d.ts +4 -0
  155. package/dist/config/yamlParsePosition.js +9 -0
  156. package/dist/hosted/containment.d.ts +3 -0
  157. package/dist/hosted/containment.js +36 -0
  158. package/dist/hosted/errors.d.ts +21 -0
  159. package/dist/hosted/errors.js +30 -0
  160. package/dist/hosted/packageSkills.d.ts +3 -0
  161. package/dist/hosted/packageSkills.js +80 -0
  162. package/dist/hosted/runtime.d.ts +12 -0
  163. package/dist/hosted/runtime.js +596 -0
  164. package/dist/hosted/sequencer.d.ts +28 -0
  165. package/dist/hosted/sequencer.js +129 -0
  166. package/dist/hosted/translate.d.ts +3 -0
  167. package/dist/hosted/translate.js +43 -0
  168. package/dist/hosted/types.d.ts +108 -0
  169. package/dist/hosted/types.js +10 -0
  170. package/dist/index.d.ts +9 -1
  171. package/dist/index.js +6 -0
  172. package/dist/mcp/getStartedTool.js +15 -6
  173. package/dist/mcp/triggerTools.js +2 -8
  174. package/dist/operatorAgent/agentPort.d.ts +11 -0
  175. package/dist/operatorAgent/agentPort.js +12 -0
  176. package/dist/operatorAgent/index.d.ts +1 -0
  177. package/dist/operatorAgent/index.js +1 -0
  178. package/dist/operatorAgent/piModel.js +26 -6
  179. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  180. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +4 -2
  181. package/dist/operatorAgent/profiles/workshopAuthorTools.js +103 -16
  182. package/dist/package-meta.d.ts +1 -1
  183. package/dist/package-meta.js +1 -1
  184. package/dist/project/ensureProjectRootsFromEnv.d.ts +3 -0
  185. package/dist/project/ensureProjectRootsFromEnv.js +22 -0
  186. package/dist/project/globalHome.d.ts +1 -0
  187. package/dist/project/globalHome.js +18 -0
  188. package/dist/project/workspaceCatalog.d.ts +10 -0
  189. package/dist/project/workspaceCatalog.js +91 -0
  190. package/dist/projection/projectRun.d.ts +0 -1
  191. package/dist/projection/projectRun.js +0 -1
  192. package/dist/runstore/backup.js +14 -4
  193. package/dist/runstore/port.d.ts +11 -1
  194. package/dist/runstore/restore.js +5 -2
  195. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -0
  196. package/dist/runstore/sqlite/SqliteRunStore.js +29 -9
  197. package/dist/runstore/sqlite/migrations/012-trigger-fires.d.ts +7 -0
  198. package/dist/runstore/sqlite/migrations/012-trigger-fires.js +18 -0
  199. package/dist/runstore/sqlite/migrations/index.js +2 -0
  200. package/dist/runstore/stageSnapshot.d.ts +3 -1
  201. package/dist/runstore/stageSnapshot.js +19 -0
  202. package/dist/runstore/workspaceLayout.js +27 -0
  203. package/dist/runstore/writeContainedArtifact.d.ts +1 -0
  204. package/dist/runstore/writeContainedArtifact.js +52 -0
  205. package/dist/runtime/containerLimits.d.ts +0 -3
  206. package/dist/runtime/containerLimits.js +0 -5
  207. package/dist/runtime/credentialBinding.d.ts +3 -0
  208. package/dist/runtime/credentialBinding.js +23 -6
  209. package/dist/runtime/emailSource.js +20 -28
  210. package/dist/runtime/githubPollSource.js +20 -28
  211. package/dist/runtime/pipelineRunner.d.ts +4 -4
  212. package/dist/runtime/pipelineScheduler.d.ts +2 -2
  213. package/dist/runtime/pipelineScheduler.js +3 -0
  214. package/dist/runtime/resumeReconstruct.d.ts +2 -2
  215. package/dist/runtime/resumeTimedOut.d.ts +2 -2
  216. package/dist/runtime/runManager.d.ts +18 -2
  217. package/dist/runtime/runManager.js +95 -30
  218. package/dist/runtime/runRetentionSweep.d.ts +0 -2
  219. package/dist/runtime/runRetentionSweep.js +0 -8
  220. package/dist/runtime/runRetryCoordinator.d.ts +2 -2
  221. package/dist/runtime/scheduleSource.js +15 -24
  222. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  223. package/dist/runtime/stageAttemptBootstrap.js +10 -1
  224. package/dist/runtime/stageProcessLauncher.js +2 -2
  225. package/dist/runtime/stageRoots.js +12 -1
  226. package/dist/runtime/stageRunner.d.ts +2 -2
  227. package/dist/runtime/stageRunner.js +8 -2
  228. package/dist/runtime/stageWorker.js +2 -2
  229. package/dist/runtime/triggerPort.d.ts +5 -3
  230. package/dist/runtime/triggerRunner.d.ts +1 -0
  231. package/dist/runtime/triggerRunner.js +27 -20
  232. package/dist/runtime/webhookAccept.d.ts +25 -0
  233. package/dist/runtime/webhookAccept.js +56 -0
  234. package/dist/server/bootstrap.d.ts +7 -2
  235. package/dist/server/bootstrap.js +15 -1
  236. package/dist/server/controlToken.d.ts +1 -1
  237. package/dist/server/controlToken.js +18 -2
  238. package/dist/server/healthSurfaces.d.ts +12 -0
  239. package/dist/server/healthSurfaces.js +27 -6
  240. package/dist/server/http.d.ts +19 -10
  241. package/dist/server/http.js +605 -85
  242. package/dist/server/liveViewPath.d.ts +9 -0
  243. package/dist/server/liveViewPath.js +8 -0
  244. package/dist/server/liveViewRoutes.d.ts +58 -0
  245. package/dist/server/liveViewRoutes.js +565 -0
  246. package/dist/server/mcpHost.d.ts +4 -2
  247. package/dist/server/mcpHost.js +2 -0
  248. package/dist/server/readCatalogFile.d.ts +29 -0
  249. package/dist/server/readCatalogFile.js +215 -0
  250. package/dist/server/triggerRoutes.d.ts +1 -0
  251. package/dist/server/triggerRoutes.js +200 -73
  252. package/dist/tools/readEmail.d.ts +3 -3
  253. package/dist/tools/writeStageArtifact.d.ts +9 -0
  254. package/dist/tools/writeStageArtifact.js +2 -47
  255. package/dist/types/pipeline.d.ts +1 -1
  256. package/dist/types/stage.d.ts +1 -1
  257. package/dist/types/stageflowManifest.d.ts +1 -1
  258. package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  259. package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  260. package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  261. package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  262. package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  263. package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  264. package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  265. package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  266. package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  267. package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  268. package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  269. package/dist/ui/assets/index-BoFPkKFg.js +256 -0
  270. package/dist/ui/assets/{index-Bv4p1aPK.css → index-CIPbbBUi.css} +1 -1
  271. package/dist/ui/index.html +4 -4
  272. package/dist/workshop/autosave.d.ts +2 -2
  273. package/dist/workshop/autosave.js +4 -6
  274. package/dist/workshop/buildStore.d.ts +2 -0
  275. package/dist/workshop/chatAttachments.d.ts +27 -0
  276. package/dist/workshop/chatAttachments.js +98 -0
  277. package/dist/workshop/chatTurn.d.ts +5 -2
  278. package/dist/workshop/chatTurn.js +22 -18
  279. package/dist/workshop/sessionMutations.d.ts +17 -0
  280. package/dist/workshop/sessionMutations.js +107 -0
  281. package/dist/workshop/sessionStore.d.ts +10 -0
  282. package/dist/workshop/sessionStore.js +52 -0
  283. package/dist/workshop/workshopPin.d.ts +4 -3
  284. package/dist/workshop/workshopPin.js +26 -6
  285. package/dist/workshop/workshopTranscriptTurn.d.ts +8 -0
  286. package/dist/workshop/workshopTranscriptTurn.js +46 -0
  287. package/examples/README.md +1 -1
  288. package/examples/archify-on-pr/README.md +1 -1
  289. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +0 -1
  290. package/examples/archify-on-pr/author-diagrams.yaml +1 -0
  291. package/examples/conditional-fork/branch-a.yaml +1 -1
  292. package/examples/conditional-fork/decide.yaml +2 -1
  293. package/examples/feature-loop/{implement.yaml → feature-implement.yaml} +1 -1
  294. package/examples/feature-loop/feature-loop.pipeline.yaml +10 -10
  295. package/examples/feature-loop/{plan.yaml → feature-plan.yaml} +1 -1
  296. package/examples/feature-loop/{review.yaml → feature-review.yaml} +1 -1
  297. package/examples/feedback-loop/{implement.yaml → feedback-implement.yaml} +1 -1
  298. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +9 -9
  299. package/examples/feedback-loop/feedback-loop.pipeline.yaml +9 -9
  300. package/examples/feedback-loop/{plan.yaml → feedback-plan.yaml} +1 -1
  301. package/examples/feedback-loop/{review.yaml → feedback-review.yaml} +1 -1
  302. package/examples/generic-fan-in/{research.yaml → fan-in-research.yaml} +1 -1
  303. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +3 -3
  304. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +3 -3
  305. package/examples/hello-world/{research.yaml → hello-research.yaml} +1 -1
  306. package/examples/hello-world/hello.pipeline.yaml +2 -2
  307. package/examples/mcp-hitl-tour/{review.yaml → mcp-hitl-review.yaml} +1 -1
  308. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +3 -3
  309. package/examples/oss-issue-contribution/oss-explain-fix.yaml +1 -0
  310. package/examples/oss-issue-contribution/oss-explain-issue.yaml +1 -0
  311. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +0 -2
  312. package/examples/stageflow.yaml +6 -0
  313. package/examples/triggers/sample-github-pr.trigger.yaml +12 -0
  314. package/examples/triggers/sample-inbox-email.trigger.yaml +14 -0
  315. package/examples/triggers/sample-manual-hello.trigger.yaml +5 -0
  316. package/examples/triggers/sample-nightly-hello.trigger.yaml +8 -0
  317. package/examples/triggers/sample-signed-webhook.trigger.yaml +11 -0
  318. package/examples/triggers-readiness/README.md +48 -0
  319. package/examples/triggers-readiness/readiness-emit.yaml +18 -0
  320. package/examples/triggers-readiness/readiness.pipeline.yaml +4 -0
  321. package/examples/triggers-readiness/readiness.task.yaml +6 -0
  322. package/examples/triggers-readiness/stageflow.yaml +13 -0
  323. package/examples/triggers-readiness/triggers/readiness-dynamic-manual.trigger.yaml +4 -0
  324. package/examples/triggers-readiness/triggers/readiness-email.trigger.yaml +12 -0
  325. package/examples/triggers-readiness/triggers/readiness-github-pr.trigger.yaml +12 -0
  326. package/examples/triggers-readiness/triggers/readiness-manual.trigger.yaml +5 -0
  327. package/examples/triggers-readiness/triggers/readiness-schedule.trigger.yaml +8 -0
  328. package/examples/triggers-readiness/triggers/readiness-webhook.trigger.yaml +11 -0
  329. package/package.json +9 -1
  330. package/skills/stageflow/references/control-surface.md +1 -1
  331. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  332. package/skills/stageflow-run/SKILL.md +1 -1
  333. package/skills/stageflow-run/references/mcp-call.md +1 -1
  334. package/skills/stageflow-setup/SKILL.md +1 -1
  335. package/dist/ui/assets/index-59cjF0OA.js +0 -133
@@ -0,0 +1,161 @@
1
+ import { access, mkdir, realpath, unlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { stringify as stringifyYaml } from "yaml";
4
+ import { getCatalogScanPaths } from "./browseCatalog.js";
5
+ import { STAGE_ID_PATTERN } from "./createStage.js";
6
+ import { loadTaskOutcome } from "./loadTask.js";
7
+ import { catalogContextFromStageflow } from "./resolveCatalogContext.js";
8
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
9
+ const OPTIONAL_KEYS = ["context", "constraints", "checkout", "repository", "ref"];
10
+ function isPlainObject(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ export function validateTaskId(id) {
14
+ if (id.length === 0 || id.length > 64) {
15
+ return "id must be 1-64 characters";
16
+ }
17
+ if (!STAGE_ID_PATTERN.test(id)) {
18
+ return "id must be lowercase kebab-case";
19
+ }
20
+ return null;
21
+ }
22
+ function optionalCreateString(body, key) {
23
+ if (!Object.hasOwn(body, key) || body[key] === undefined || body[key] === null) {
24
+ return { ok: true };
25
+ }
26
+ if (typeof body[key] !== "string") {
27
+ return { ok: false, error: `${key} must be a string` };
28
+ }
29
+ if (body[key].length === 0)
30
+ return { ok: true };
31
+ return { ok: true, value: body[key] };
32
+ }
33
+ export function parseCreateTaskBody(body) {
34
+ if (!isPlainObject(body)) {
35
+ return { ok: false, status: 400, error: "Request body must be an object" };
36
+ }
37
+ if (typeof body.directory !== "string" || body.directory.trim().length === 0) {
38
+ return { ok: false, status: 400, error: "directory is required" };
39
+ }
40
+ const directory = body.directory.trim().replace(/\\/g, "/").replace(/\/+$/, "");
41
+ if (directory.length === 0) {
42
+ return { ok: false, status: 400, error: "directory is required" };
43
+ }
44
+ if (typeof body.id !== "string") {
45
+ return { ok: false, status: 400, error: "id is required" };
46
+ }
47
+ const idError = validateTaskId(body.id);
48
+ if (idError) {
49
+ return { ok: false, status: 400, error: idError };
50
+ }
51
+ if (typeof body.goal !== "string" || body.goal.length === 0) {
52
+ return { ok: false, status: 400, error: "goal is required" };
53
+ }
54
+ const input = {
55
+ directory,
56
+ id: body.id,
57
+ goal: body.goal,
58
+ };
59
+ for (const key of OPTIONAL_KEYS) {
60
+ const parsed = optionalCreateString(body, key);
61
+ if (!parsed.ok) {
62
+ return { ok: false, status: 400, error: parsed.error };
63
+ }
64
+ if (parsed.value !== undefined)
65
+ input[key] = parsed.value;
66
+ }
67
+ return input;
68
+ }
69
+ export function resolveTaskDirectory(projectRoot, directory) {
70
+ const absDirectory = path.resolve(projectRoot, directory);
71
+ const rel = path.relative(projectRoot, absDirectory);
72
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
73
+ return null;
74
+ }
75
+ return absDirectory;
76
+ }
77
+ async function fileExists(filePath) {
78
+ try {
79
+ await access(filePath);
80
+ return true;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ export function taskFieldsToYaml(fields) {
87
+ const doc = {
88
+ id: fields.id,
89
+ goal: fields.goal,
90
+ };
91
+ for (const key of OPTIONAL_KEYS) {
92
+ const value = fields[key];
93
+ if (typeof value === "string" && value.length > 0)
94
+ doc[key] = value;
95
+ }
96
+ const text = stringifyYaml(doc, { indent: 2, lineWidth: 0 });
97
+ return text.endsWith("\n") ? text : `${text}\n`;
98
+ }
99
+ export async function findTaskIdCollision(projectRoot, id, targetPath) {
100
+ if (await fileExists(targetPath)) {
101
+ return path.relative(projectRoot, targetPath).replace(/\\/g, "/");
102
+ }
103
+ const ctx = catalogContextFromStageflow(await resolveStageflowContext(projectRoot));
104
+ const scanPaths = await getCatalogScanPaths(ctx);
105
+ if (!scanPaths)
106
+ return null;
107
+ const resolvedTarget = path.resolve(targetPath);
108
+ for (const filePath of scanPaths.taskPaths) {
109
+ if (path.resolve(filePath) === resolvedTarget)
110
+ continue;
111
+ const outcome = await loadTaskOutcome(filePath);
112
+ if (outcome.ok && outcome.value.id === id) {
113
+ return path.relative(projectRoot, filePath).replace(/\\/g, "/");
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ function taskDetail(task, projectRoot, filePath) {
119
+ return {
120
+ ...task,
121
+ path: path.relative(projectRoot, filePath).replace(/\\/g, "/"),
122
+ };
123
+ }
124
+ export async function createTask(rawProjectRoot, input) {
125
+ const idError = validateTaskId(input.id);
126
+ if (idError) {
127
+ return { ok: false, status: 400, error: idError };
128
+ }
129
+ const projectRoot = await realpath(rawProjectRoot).catch(() => rawProjectRoot);
130
+ const directory = resolveTaskDirectory(projectRoot, input.directory);
131
+ if (!directory) {
132
+ return { ok: false, status: 400, error: "directory must be inside the project root" };
133
+ }
134
+ const filePath = path.join(directory, `${input.id}.task.yaml`);
135
+ const collision = await findTaskIdCollision(projectRoot, input.id, filePath);
136
+ if (collision) {
137
+ const targetRel = path.relative(projectRoot, filePath).replace(/\\/g, "/");
138
+ const error = collision === targetRel
139
+ ? `Task file already exists (${collision})`
140
+ : `Task id already exists (${collision})`;
141
+ return { ok: false, status: 409, error };
142
+ }
143
+ try {
144
+ await mkdir(directory, { recursive: true });
145
+ await writeFile(filePath, taskFieldsToYaml(input), "utf8");
146
+ }
147
+ catch (err) {
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ return { ok: false, status: 500, error: message };
150
+ }
151
+ const outcome = await loadTaskOutcome(filePath);
152
+ if (!outcome.ok) {
153
+ await unlink(filePath).catch(() => { });
154
+ return {
155
+ ok: false,
156
+ status: 400,
157
+ error: outcome.issues[0]?.message ?? "Invalid task",
158
+ };
159
+ }
160
+ return { ok: true, task: taskDetail(outcome.value, projectRoot, filePath) };
161
+ }
@@ -23,5 +23,20 @@ export type CreateTriggerResult = {
23
23
  status: 400 | 409 | 422 | 500;
24
24
  error: string;
25
25
  };
26
+ type ParsedField<T> = {
27
+ ok: true;
28
+ value: T | undefined;
29
+ } | {
30
+ ok: false;
31
+ message: string;
32
+ };
33
+ export declare function parseScheduleField(raw: unknown): ParsedField<TriggerSchedule>;
34
+ export declare function parseEventField(raw: unknown): ParsedField<TriggerEvent>;
26
35
  export declare function parseCreateTriggerBody(body: unknown): CreateTriggerInput | CreateTriggerParseError;
36
+ export declare function scheduleCronError(schedule: TriggerSchedule): string | null;
37
+ export declare function triggerReferenceError(scanPaths: {
38
+ pipelinePaths: string[];
39
+ taskPaths: string[];
40
+ }, pipeline: string, task: string | undefined): Promise<string | null>;
27
41
  export declare function createTrigger(rawProjectRoot: string, input: CreateTriggerInput): Promise<CreateTriggerResult>;
42
+ export {};
@@ -21,7 +21,7 @@ function validateTriggerId(id) {
21
21
  }
22
22
  return null;
23
23
  }
24
- function parseScheduleField(raw) {
24
+ export function parseScheduleField(raw) {
25
25
  if (raw === undefined)
26
26
  return { ok: true, value: undefined };
27
27
  if (!isPlainObject(raw)) {
@@ -41,7 +41,7 @@ function parseScheduleField(raw) {
41
41
  },
42
42
  };
43
43
  }
44
- function parseEventField(raw) {
44
+ export function parseEventField(raw) {
45
45
  if (raw === undefined)
46
46
  return { ok: true, value: undefined };
47
47
  if (!isPlainObject(raw)) {
@@ -129,6 +129,29 @@ function resolveDirectory(projectRoot, directory) {
129
129
  }
130
130
  return absDirectory;
131
131
  }
132
+ export function scheduleCronError(schedule) {
133
+ try {
134
+ new Cron(schedule.cron, schedule.timezone !== undefined ? { timezone: schedule.timezone } : {});
135
+ return null;
136
+ }
137
+ catch (err) {
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ return `Invalid schedule.cron: ${message}`;
140
+ }
141
+ }
142
+ export async function triggerReferenceError(scanPaths, pipeline, task) {
143
+ const pipelineIds = await collectPipelineIdsFromPaths(scanPaths.pipelinePaths);
144
+ if (!pipelineIds.has(pipeline)) {
145
+ return `Trigger references unknown pipeline "${pipeline}"`;
146
+ }
147
+ if (task !== undefined) {
148
+ const taskIds = await collectTaskIdsFromPaths(scanPaths.taskPaths);
149
+ if (!taskIds.has(task)) {
150
+ return `Trigger references unknown task "${task}"`;
151
+ }
152
+ }
153
+ return null;
154
+ }
132
155
  function triggerInputToYaml(input, enabled) {
133
156
  const doc = {
134
157
  id: input.id,
@@ -172,34 +195,17 @@ export async function createTrigger(rawProjectRoot, input) {
172
195
  error: `Trigger id already exists (${path.relative(projectRoot, collisionPath).replace(/\\/g, "/")})`,
173
196
  };
174
197
  }
175
- const pipelineIds = await collectPipelineIdsFromPaths(scanPaths.pipelinePaths);
176
- if (!pipelineIds.has(input.pipeline)) {
177
- return {
178
- ok: false,
179
- status: 422,
180
- error: `Trigger references unknown pipeline "${input.pipeline}"`,
181
- };
182
- }
183
- if (input.task !== undefined) {
184
- const taskIds = await collectTaskIdsFromPaths(scanPaths.taskPaths);
185
- if (!taskIds.has(input.task)) {
186
- return {
187
- ok: false,
188
- status: 422,
189
- error: `Trigger references unknown task "${input.task}"`,
190
- };
191
- }
198
+ const refError = await triggerReferenceError(scanPaths, input.pipeline, input.task);
199
+ if (refError) {
200
+ return { ok: false, status: 422, error: refError };
192
201
  }
193
202
  if (input.kind === "schedule") {
194
203
  if (!input.schedule) {
195
204
  return { ok: false, status: 422, error: "schedule.cron is required for kind=schedule" };
196
205
  }
197
- try {
198
- new Cron(input.schedule.cron, input.schedule.timezone !== undefined ? { timezone: input.schedule.timezone } : {});
199
- }
200
- catch (err) {
201
- const message = err instanceof Error ? err.message : String(err);
202
- return { ok: false, status: 422, error: `Invalid schedule.cron: ${message}` };
206
+ const cronError = scheduleCronError(input.schedule);
207
+ if (cronError) {
208
+ return { ok: false, status: 422, error: cronError };
203
209
  }
204
210
  }
205
211
  const filePath = path.join(directory, `${input.id}.trigger.yaml`);
@@ -227,6 +233,7 @@ export async function createTrigger(rawProjectRoot, input) {
227
233
  trigger: {
228
234
  ...loadOutcome.value,
229
235
  definition_ref: relPath,
236
+ project_root: projectRoot,
230
237
  },
231
238
  };
232
239
  }
@@ -81,9 +81,50 @@ export type LoadTaskArtifactResult = {
81
81
  status: 400 | 404;
82
82
  error: string;
83
83
  };
84
+ /** Move uses-wrapper skill: onto the matching stage artifact body, then clear the wrapper. */
85
+ export declare function hoistUsesWrapperSkills(draft: DraftPackage): DraftPackage;
84
86
  export declare function validateDraftPackage(draft: DraftPackage, options?: ValidateDraftPackageOptions): Promise<ValidationResult>;
85
87
  export declare function overwriteDraftPackage(projectRoot: string, input: OverwriteDraftPackageInput): Promise<OverwriteDraftPackageResult>;
86
88
  export declare function createDraftPackage(projectRoot: string, input: CreateDraftPackageInput): Promise<CreateDraftPackageResult>;
89
+ export type DraftPlanMode = "create" | "overwrite";
90
+ export type DraftPlanFile = {
91
+ path: string;
92
+ kind: "pipeline" | "stage" | "task";
93
+ action: "new" | "overwrite" | "unchanged";
94
+ added: number;
95
+ removed: number;
96
+ };
97
+ export type DraftPlanResult = {
98
+ pipelinePath: string;
99
+ directory: string;
100
+ files: DraftPlanFile[];
101
+ pipelineIdTaken: boolean;
102
+ };
103
+ export type PlanDraftPackageOptions = {
104
+ mode?: DraftPlanMode;
105
+ catalogPipelines?: ReadonlyArray<{
106
+ id: string;
107
+ path: string;
108
+ }>;
109
+ };
110
+ export declare function countLineChanges(before: string, after: string): {
111
+ added: number;
112
+ removed: number;
113
+ };
114
+ export declare function planDraftPackage(projectRoot: string, input: CreateDraftPackageInput, options?: PlanDraftPackageOptions): Promise<({
115
+ ok: true;
116
+ } & DraftPlanResult) | {
117
+ ok: false;
118
+ status: 400;
119
+ error: string;
120
+ }>;
121
+ export declare function parsePlanDraftPackageBody(body: unknown): (CreateDraftPackageInput & {
122
+ mode: DraftPlanMode;
123
+ }) | {
124
+ ok: false;
125
+ status: 400;
126
+ error: string;
127
+ };
87
128
  export declare function parseDraftPackageBody(body: unknown): DraftPackage | {
88
129
  ok: false;
89
130
  status: 400;
@@ -1,4 +1,4 @@
1
- import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
1
+ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { loadPipelineOutcome } from "./loadPipeline.js";
@@ -6,6 +6,8 @@ import { parseTaskFile } from "./loadTask.js";
6
6
  import { stringifyTargetYaml } from "./printTargetYaml.js";
7
7
  import { readYamlObject } from "./readYamlObject.js";
8
8
  import { buildValidationResult, findingsFromLoadIssues, loadPipelineValidated, validatePipeline, } from "./validateCatalog.js";
9
+ import { normalizeDraftCatalogFilenames, taskIdFromDraftTask, } from "./workshopCatalogFilenames.js";
10
+ import { reconcileWorkshopCatalogPackageDir } from "./workshopCatalogRepair.js";
9
11
  function isPlainObject(value) {
10
12
  return value !== null && typeof value === "object" && !Array.isArray(value);
11
13
  }
@@ -15,10 +17,57 @@ function normalizeStageRelPath(stagePath) {
15
17
  function pipelineHasExternalUses(pipeline) {
16
18
  return pipeline.stages.some((stage) => typeof stage.uses === "string" && stage.uses.trim().length > 0);
17
19
  }
20
+ function stripUsesWrapperSkill(stages) {
21
+ return stages.map((stage) => {
22
+ if (typeof stage.uses !== "string" || stage.skill === undefined)
23
+ return stage;
24
+ const { skill: _skill, ...rest } = stage;
25
+ return rest;
26
+ });
27
+ }
28
+ /** Move uses-wrapper skill: onto the matching stage artifact body, then clear the wrapper. */
29
+ export function hoistUsesWrapperSkills(draft) {
30
+ const artifacts = draft.stages ? [...draft.stages] : [];
31
+ const byPath = new Map(artifacts.map((artifact, index) => [
32
+ normalizeStageRelPath(artifact.path),
33
+ index,
34
+ ]));
35
+ const stages = draft.pipeline.stages.map((stage) => {
36
+ if (typeof stage.uses !== "string" || stage.skill === undefined)
37
+ return stage;
38
+ const skill = typeof stage.skill === "string" && stage.skill.trim()
39
+ ? stage.skill.trim()
40
+ : undefined;
41
+ if (skill === undefined) {
42
+ const { skill: _skill, ...rest } = stage;
43
+ return rest;
44
+ }
45
+ const key = normalizeStageRelPath(stage.uses);
46
+ const index = byPath.get(key);
47
+ if (index !== undefined) {
48
+ const current = artifacts[index];
49
+ const bodySkill = current.body.skill;
50
+ const bodyHasSkill = typeof bodySkill === "string" && bodySkill.trim().length > 0;
51
+ if (!bodyHasSkill) {
52
+ artifacts[index] = {
53
+ ...current,
54
+ body: { ...current.body, skill },
55
+ };
56
+ }
57
+ }
58
+ const { skill: _skill, ...rest } = stage;
59
+ return rest;
60
+ });
61
+ return {
62
+ ...draft,
63
+ pipeline: { ...draft.pipeline, stages },
64
+ ...(artifacts.length > 0 ? { stages: artifacts } : {}),
65
+ };
66
+ }
18
67
  function pipelineDocument(pipeline) {
19
68
  const doc = {
20
69
  id: pipeline.id,
21
- stages: pipeline.stages,
70
+ stages: stripUsesWrapperSkill(pipeline.stages),
22
71
  };
23
72
  if (pipeline.agent !== undefined)
24
73
  doc.agent = pipeline.agent;
@@ -48,9 +97,10 @@ async function fileExists(filePath) {
48
97
  }
49
98
  }
50
99
  async function materializeDraft(draft, dir, pipelineFilename) {
100
+ const migrated = hoistUsesWrapperSkills(draft);
51
101
  await mkdir(dir, { recursive: true });
52
- await writeFile(path.join(dir, pipelineFilename), stringifyTargetYaml(pipelineDocument(draft.pipeline)), "utf8");
53
- for (const stage of draft.stages ?? []) {
102
+ await writeFile(path.join(dir, pipelineFilename), stringifyTargetYaml(pipelineDocument(migrated.pipeline)), "utf8");
103
+ for (const stage of migrated.stages ?? []) {
54
104
  const rel = normalizeStageRelPath(stage.path);
55
105
  const stagePath = path.join(dir, rel);
56
106
  await mkdir(path.dirname(stagePath), { recursive: true });
@@ -185,11 +235,13 @@ function resolveDraftWriteTarget(projectRoot, input) {
185
235
  error: "directory must be inside the project root",
186
236
  };
187
237
  }
188
- const draft = input.draft;
189
- if (!isPlainObject(draft?.pipeline) || typeof draft.pipeline.id !== "string") {
238
+ const rawDraft = input.draft;
239
+ if (!isPlainObject(rawDraft?.pipeline) || typeof rawDraft.pipeline.id !== "string") {
190
240
  return { ok: false, status: 400, error: "draft.pipeline.id is required" };
191
241
  }
192
- const pipelineFilename = input.pipelineFilename?.trim() || `${draft.pipeline.id}.pipeline.yaml`;
242
+ const normalized = normalizeDraftCatalogFilenames(rawDraft, input.pipelineFilename?.trim());
243
+ const draft = hoistUsesWrapperSkills(normalized.draft);
244
+ const pipelineFilename = normalized.pipelineFilename;
193
245
  const pipelineAbsPath = path.join(packageDirectory, path.basename(pipelineFilename));
194
246
  const pipelineRelPath = path
195
247
  .relative(projectRoot, pipelineAbsPath)
@@ -223,9 +275,17 @@ async function gateDraftWrite(projectRoot, draft, allowInvalid) {
223
275
  findings: validation.findings,
224
276
  };
225
277
  }
226
- async function writeDraftPackageFiles(projectRoot, target, allowInvalid) {
278
+ function resolveDraftWriteFiles(projectRoot, target) {
227
279
  const { packageDirectory, draft, pipelineAbsPath, pipelineRelPath } = target;
228
- const stagePaths = [];
280
+ const toRel = (abs) => path.relative(projectRoot, abs).replace(/\\/g, "/");
281
+ const files = [
282
+ {
283
+ kind: "pipeline",
284
+ absPath: pipelineAbsPath,
285
+ relPath: pipelineRelPath,
286
+ content: stringifyTargetYaml(pipelineDocument(draft.pipeline)),
287
+ },
288
+ ];
229
289
  for (const stage of draft.stages ?? []) {
230
290
  const rel = normalizeStageRelPath(stage.path);
231
291
  if (!rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
@@ -244,23 +304,39 @@ async function writeDraftPackageFiles(projectRoot, target, allowInvalid) {
244
304
  error: `Stage path escapes package directory (${stage.path})`,
245
305
  };
246
306
  }
307
+ files.push({
308
+ kind: "stage",
309
+ absPath: stageAbs,
310
+ relPath: toRel(stageAbs),
311
+ content: stringifyTargetYaml(stage.body),
312
+ });
247
313
  }
314
+ if (draft.task) {
315
+ const taskAbs = path.join(packageDirectory, path.basename(draft.task.filename));
316
+ files.push({
317
+ kind: "task",
318
+ absPath: taskAbs,
319
+ relPath: toRel(taskAbs),
320
+ content: stringifyTargetYaml(draft.task.body),
321
+ });
322
+ }
323
+ return { ok: true, files };
324
+ }
325
+ async function writeDraftPackageFiles(projectRoot, target, allowInvalid) {
326
+ const { packageDirectory, draft, pipelineRelPath } = target;
327
+ const resolved = resolveDraftWriteFiles(projectRoot, target);
328
+ if (!resolved.ok)
329
+ return resolved;
248
330
  try {
249
331
  await mkdir(packageDirectory, { recursive: true });
250
- await writeFile(pipelineAbsPath, stringifyTargetYaml(pipelineDocument(draft.pipeline)), "utf8");
251
- for (const stage of draft.stages ?? []) {
252
- const rel = normalizeStageRelPath(stage.path);
253
- const stageAbs = path.resolve(packageDirectory, rel);
254
- await mkdir(path.dirname(stageAbs), { recursive: true });
255
- await writeFile(stageAbs, stringifyTargetYaml(stage.body), "utf8");
256
- stagePaths.push(path.relative(projectRoot, stageAbs).replace(/\\/g, "/"));
257
- }
258
- let taskPath;
259
- if (draft.task) {
260
- const taskAbs = path.join(packageDirectory, path.basename(draft.task.filename));
261
- await writeFile(taskAbs, stringifyTargetYaml(draft.task.body), "utf8");
262
- taskPath = path.relative(projectRoot, taskAbs).replace(/\\/g, "/");
332
+ for (const file of resolved.files) {
333
+ await mkdir(path.dirname(file.absPath), { recursive: true });
334
+ await writeFile(file.absPath, file.content, "utf8");
263
335
  }
336
+ const stagePaths = resolved.files
337
+ .filter((file) => file.kind === "stage")
338
+ .map((file) => file.relPath);
339
+ const taskPath = resolved.files.find((file) => file.kind === "task")?.relPath;
264
340
  const loadOutcome = await loadPipelineOutcome(pipelineRelPath, {
265
341
  cwd: projectRoot,
266
342
  });
@@ -274,6 +350,11 @@ async function writeDraftPackageFiles(projectRoot, target, allowInvalid) {
274
350
  error: loadOutcome.issues[0]?.message ?? "Failed to load written pipeline",
275
351
  };
276
352
  }
353
+ const taskId = draft.task ? taskIdFromDraftTask(draft.task) : null;
354
+ await reconcileWorkshopCatalogPackageDir(packageDirectory, draft.pipeline.id, taskId, {
355
+ pipelineBasename: path.basename(target.pipelineAbsPath),
356
+ taskBasename: draft.task ? path.basename(draft.task.filename) : null,
357
+ });
277
358
  return {
278
359
  ok: true,
279
360
  pipeline,
@@ -287,10 +368,22 @@ async function writeDraftPackageFiles(projectRoot, target, allowInvalid) {
287
368
  return { ok: false, status: 500, error: message };
288
369
  }
289
370
  }
371
+ async function prepareDraftPackageDirectory(target) {
372
+ const taskId = target.draft.task
373
+ ? taskIdFromDraftTask(target.draft.task)
374
+ : null;
375
+ await reconcileWorkshopCatalogPackageDir(target.packageDirectory, target.draft.pipeline.id, taskId, {
376
+ pipelineBasename: path.basename(target.pipelineAbsPath),
377
+ taskBasename: target.draft.task
378
+ ? path.basename(path.join(target.packageDirectory, path.basename(target.draft.task.filename)))
379
+ : null,
380
+ });
381
+ }
290
382
  export async function overwriteDraftPackage(projectRoot, input) {
291
383
  const resolved = resolveDraftWriteTarget(projectRoot, input);
292
384
  if (!resolved.ok)
293
385
  return resolved;
386
+ await prepareDraftPackageDirectory(resolved.value);
294
387
  if (!(await fileExists(resolved.value.pipelineAbsPath))) {
295
388
  return {
296
389
  ok: false,
@@ -307,6 +400,7 @@ export async function createDraftPackage(projectRoot, input) {
307
400
  const resolved = resolveDraftWriteTarget(projectRoot, input);
308
401
  if (!resolved.ok)
309
402
  return resolved;
403
+ await prepareDraftPackageDirectory(resolved.value);
310
404
  if (await fileExists(resolved.value.pipelineAbsPath)) {
311
405
  return {
312
406
  ok: false,
@@ -319,6 +413,104 @@ export async function createDraftPackage(projectRoot, input) {
319
413
  return blocked;
320
414
  return writeDraftPackageFiles(projectRoot, resolved.value, input.allowInvalid);
321
415
  }
416
+ function splitLines(text) {
417
+ if (!text)
418
+ return [];
419
+ return (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
420
+ }
421
+ export function countLineChanges(before, after) {
422
+ const a = splitLines(before);
423
+ const b = splitLines(after);
424
+ let start = 0;
425
+ while (start < a.length && start < b.length && a[start] === b[start])
426
+ start += 1;
427
+ let endA = a.length;
428
+ let endB = b.length;
429
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
430
+ endA -= 1;
431
+ endB -= 1;
432
+ }
433
+ const midA = a.slice(start, endA);
434
+ const midB = b.slice(start, endB);
435
+ let prev = new Uint32Array(midB.length + 1);
436
+ let curr = new Uint32Array(midB.length + 1);
437
+ for (const lineA of midA) {
438
+ for (let j = 1; j <= midB.length; j += 1) {
439
+ curr[j] =
440
+ lineA === midB[j - 1] ? prev[j - 1] + 1 : Math.max(prev[j], curr[j - 1]);
441
+ }
442
+ [prev, curr] = [curr, prev];
443
+ }
444
+ const common = prev[midB.length];
445
+ return { added: midB.length - common, removed: midA.length - common };
446
+ }
447
+ async function readExisting(filePath) {
448
+ try {
449
+ return await readFile(filePath, "utf8");
450
+ }
451
+ catch {
452
+ return null;
453
+ }
454
+ }
455
+ export async function planDraftPackage(projectRoot, input, options = {}) {
456
+ const resolved = resolveDraftWriteTarget(projectRoot, input);
457
+ if (!resolved.ok)
458
+ return resolved;
459
+ const writeFiles = resolveDraftWriteFiles(projectRoot, resolved.value);
460
+ if (!writeFiles.ok)
461
+ return writeFiles;
462
+ const files = [];
463
+ for (const file of writeFiles.files) {
464
+ const existing = await readExisting(file.absPath);
465
+ if (existing === null) {
466
+ files.push({
467
+ path: file.relPath,
468
+ kind: file.kind,
469
+ action: "new",
470
+ added: splitLines(file.content).length,
471
+ removed: 0,
472
+ });
473
+ }
474
+ else if (existing === file.content) {
475
+ files.push({
476
+ path: file.relPath,
477
+ kind: file.kind,
478
+ action: "unchanged",
479
+ added: 0,
480
+ removed: 0,
481
+ });
482
+ }
483
+ else {
484
+ files.push({
485
+ path: file.relPath,
486
+ kind: file.kind,
487
+ action: "overwrite",
488
+ ...countLineChanges(existing, file.content),
489
+ });
490
+ }
491
+ }
492
+ const { pipelineRelPath, packageDirectory, draft } = resolved.value;
493
+ const pipelineIdTaken = (options.mode ?? "create") === "create" &&
494
+ (options.catalogPipelines ?? []).some((listing) => listing.id === draft.pipeline.id &&
495
+ listing.path.replace(/\\/g, "/") !== pipelineRelPath);
496
+ return {
497
+ ok: true,
498
+ pipelinePath: pipelineRelPath,
499
+ directory: path.relative(projectRoot, packageDirectory).replace(/\\/g, "/") || ".",
500
+ files,
501
+ pipelineIdTaken,
502
+ };
503
+ }
504
+ export function parsePlanDraftPackageBody(body) {
505
+ const parsed = parseCreateDraftPackageBody(body);
506
+ if ("ok" in parsed)
507
+ return parsed;
508
+ const mode = isPlainObject(body) ? body.mode : undefined;
509
+ if (mode !== undefined && mode !== "create" && mode !== "overwrite") {
510
+ return { ok: false, status: 400, error: 'mode must be "create" or "overwrite"' };
511
+ }
512
+ return { ...parsed, mode: mode ?? "create" };
513
+ }
322
514
  export function parseDraftPackageBody(body) {
323
515
  if (!isPlainObject(body)) {
324
516
  return { ok: false, status: 400, error: "JSON object body required" };
@@ -540,9 +732,10 @@ export async function loadDraftPackage(projectRoot, pipelineRelPath, options = {
540
732
  draft.task = loaded.task;
541
733
  attachedTaskPath = loaded.taskPath;
542
734
  }
735
+ const migrated = hoistUsesWrapperSkills(draft);
543
736
  return {
544
737
  ok: true,
545
- draft,
738
+ draft: migrated,
546
739
  destination: { directory, pipelineFilename },
547
740
  pipelinePath,
548
741
  ...(attachedTaskPath !== undefined ? { taskPath: attachedTaskPath } : {}),
@@ -1,8 +1,9 @@
1
+ import type { CatalogRootKind } from "./resolveCatalogRoots.js";
1
2
  /** Directory scanned for workshop-authored pipelines and tasks. */
2
3
  export declare const WORKSHOP_PACKAGE_ROOT = "workshop";
3
4
  export declare function normalizeCatalogDir(directory: string): string;
4
5
  /** Untitled workshop saves land here so Run can see them without a new catalog edit. */
5
- export declare function defaultWorkshopPackageDirectory(pipelineId: string | undefined): string;
6
+ export declare function defaultWorkshopPackageDirectory(pipelineId: string | undefined, catalogRootKind?: CatalogRootKind): string;
6
7
  /**
7
8
  * Catalog root that covers this package directory.
8
9
  * Packages under `workshop/` share one root. Any other folder is listed itself