stageflow 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/README.md +12 -3
  2. package/dist/agent/activity.d.ts +18 -0
  3. package/dist/agent/claudeAdapter.js +7 -0
  4. package/dist/agent/claudeTools.d.ts +27 -18
  5. package/dist/agent/claudeTools.js +54 -15
  6. package/dist/agent/compactionEvents.d.ts +34 -0
  7. package/dist/agent/compactionEvents.js +37 -0
  8. package/dist/agent/openRouterRouting.d.ts +7 -0
  9. package/dist/agent/openRouterRouting.js +23 -0
  10. package/dist/agent/piAdapter.d.ts +6 -0
  11. package/dist/agent/piAdapter.js +128 -27
  12. package/dist/agent/piSettings.d.ts +7 -0
  13. package/dist/agent/piSettings.js +9 -0
  14. package/dist/agent/sandboxPiTools.d.ts +3 -1
  15. package/dist/agent/sandboxPiTools.js +8 -2
  16. package/dist/agent/stageTurnDriver.d.ts +11 -2
  17. package/dist/agent/stageTurnDriver.js +45 -4
  18. package/dist/agent/toolOutputBudget.d.ts +15 -1
  19. package/dist/agent/toolOutputBudget.js +18 -0
  20. package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
  21. package/dist/agent/toolOutputBudgetExtension.js +40 -3
  22. package/dist/agents/authorChat.d.ts +1 -0
  23. package/dist/agents/authorChat.js +7 -0
  24. package/dist/agents/authorSessionStore.d.ts +3 -0
  25. package/dist/agents/authorSessionStore.js +17 -2
  26. package/dist/agents/hostContext.d.ts +1 -1
  27. package/dist/agents/runtime/piRuntime.d.ts +5 -1
  28. package/dist/agents/runtime/piRuntime.js +46 -12
  29. package/dist/agents/service.js +3 -2
  30. package/dist/agents/sessions.js +3 -1
  31. package/dist/compaction/directives.d.ts +18 -0
  32. package/dist/compaction/directives.js +119 -0
  33. package/dist/compaction/extension.d.ts +20 -0
  34. package/dist/compaction/extension.js +78 -0
  35. package/dist/compaction/mask.d.ts +18 -0
  36. package/dist/compaction/mask.js +155 -0
  37. package/dist/compaction/pin.d.ts +21 -0
  38. package/dist/compaction/pin.js +270 -0
  39. package/dist/compaction/scrub.d.ts +7 -0
  40. package/dist/compaction/scrub.js +26 -0
  41. package/dist/compaction/sessionWiring.d.ts +22 -0
  42. package/dist/compaction/sessionWiring.js +26 -0
  43. package/dist/compaction/summarize.d.ts +79 -0
  44. package/dist/compaction/summarize.js +171 -0
  45. package/dist/compaction/toolPolicy.d.ts +3 -0
  46. package/dist/compaction/toolPolicy.js +33 -0
  47. package/dist/compaction/types.d.ts +45 -0
  48. package/dist/compaction/types.js +7 -0
  49. package/dist/compaction/workingState.d.ts +56 -0
  50. package/dist/compaction/workingState.js +198 -0
  51. package/dist/config/createPipeline.js +2 -0
  52. package/dist/config/createTrigger.js +7 -1
  53. package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
  54. package/dist/config/ensureCatalogScanRoot.js +6 -0
  55. package/dist/config/evalCatalog.js +30 -2
  56. package/dist/config/githubTriggerAction.d.ts +8 -0
  57. package/dist/config/githubTriggerAction.js +29 -0
  58. package/dist/config/hostConfig.js +2 -0
  59. package/dist/config/loadEval.js +21 -3
  60. package/dist/config/pipelineNeeds.d.ts +2 -0
  61. package/dist/config/pipelineNeeds.js +16 -3
  62. package/dist/config/pipelineRoute.js +5 -4
  63. package/dist/config/validateCatalog.d.ts +1 -1
  64. package/dist/config/validateCatalog.js +12 -0
  65. package/dist/config/validateEval.js +43 -2
  66. package/dist/config/writeEval.js +2 -2
  67. package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
  68. package/dist/envelope/assertEmittedArtifacts.js +43 -0
  69. package/dist/envelope/preEmitChecks.d.ts +3 -0
  70. package/dist/evals/constants.d.ts +3 -1
  71. package/dist/evals/constants.js +3 -1
  72. package/dist/evals/engine/FakeEngine.d.ts +1 -1
  73. package/dist/evals/engine/FakeEngine.js +2 -1
  74. package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
  75. package/dist/evals/engine/promptfoo/PromptfooEngine.js +2 -1
  76. package/dist/evals/engine/promptfoo/generate.d.ts +1 -1
  77. package/dist/evals/engine/promptfoo/generate.js +11 -8
  78. package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +1 -1
  79. package/dist/evals/engine/promptfoo/rubricPrompt.js +5 -3
  80. package/dist/evals/manager.d.ts +13 -0
  81. package/dist/evals/manager.js +10 -2
  82. package/dist/evals/plan.d.ts +8 -1
  83. package/dist/evals/plan.js +28 -9
  84. package/dist/evals/planInternalAgent.d.ts +19 -0
  85. package/dist/evals/planInternalAgent.js +205 -0
  86. package/dist/evals/read/api.d.ts +12 -0
  87. package/dist/evals/read/api.js +5 -0
  88. package/dist/evals/read/list.js +10 -1
  89. package/dist/evals/read/runs.d.ts +5 -0
  90. package/dist/evals/read/runs.js +7 -1
  91. package/dist/evals/runner/caseRunner.d.ts +2 -1
  92. package/dist/evals/runner/caseRunner.js +16 -5
  93. package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
  94. package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
  95. package/dist/evals/snapshot.d.ts +10 -2
  96. package/dist/evals/snapshot.js +8 -3
  97. package/dist/evals/types.d.ts +12 -2
  98. package/dist/evals/types.js +4 -1
  99. package/dist/guide/cardContract.d.ts +133 -0
  100. package/dist/guide/cardContract.js +156 -0
  101. package/dist/guide/chat.d.ts +45 -0
  102. package/dist/guide/chat.js +378 -0
  103. package/dist/guide/knowledge/index.d.ts +14 -0
  104. package/dist/guide/knowledge/index.js +109 -0
  105. package/dist/guide/knowledge/topics/agents.d.ts +2 -0
  106. package/dist/guide/knowledge/topics/agents.js +58 -0
  107. package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
  108. package/dist/guide/knowledge/topics/bestPractices.js +67 -0
  109. package/dist/guide/knowledge/topics/connections.d.ts +2 -0
  110. package/dist/guide/knowledge/topics/connections.js +66 -0
  111. package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
  112. package/dist/guide/knowledge/topics/consoleMap.js +55 -0
  113. package/dist/guide/knowledge/topics/evals.d.ts +2 -0
  114. package/dist/guide/knowledge/topics/evals.js +72 -0
  115. package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
  116. package/dist/guide/knowledge/topics/limitations.js +84 -0
  117. package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
  118. package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
  119. package/dist/guide/knowledge/topics/memories.d.ts +2 -0
  120. package/dist/guide/knowledge/topics/memories.js +63 -0
  121. package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
  122. package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
  123. package/dist/guide/knowledge/topics/overview.d.ts +2 -0
  124. package/dist/guide/knowledge/topics/overview.js +56 -0
  125. package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
  126. package/dist/guide/knowledge/topics/pipelines.js +93 -0
  127. package/dist/guide/knowledge/topics/stages.d.ts +2 -0
  128. package/dist/guide/knowledge/topics/stages.js +54 -0
  129. package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
  130. package/dist/guide/knowledge/topics/tasks.js +52 -0
  131. package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
  132. package/dist/guide/knowledge/topics/triggers.js +68 -0
  133. package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
  134. package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
  135. package/dist/guide/knowledgeTools.d.ts +2 -0
  136. package/dist/guide/knowledgeTools.js +56 -0
  137. package/dist/guide/showUi.d.ts +6 -0
  138. package/dist/guide/showUi.js +546 -0
  139. package/dist/guide/threadStore.d.ts +48 -0
  140. package/dist/guide/threadStore.js +189 -0
  141. package/dist/guide/writeRoot.d.ts +11 -0
  142. package/dist/guide/writeRoot.js +70 -0
  143. package/dist/hosted/translate.js +2 -0
  144. package/dist/mcp/agentTools.js +12 -0
  145. package/dist/mcp/catalogTools.js +18 -20
  146. package/dist/mcp/catalogWriteTools.d.ts +8 -0
  147. package/dist/mcp/catalogWriteTools.js +225 -0
  148. package/dist/mcp/connectionTools.d.ts +3 -0
  149. package/dist/mcp/connectionTools.js +85 -0
  150. package/dist/mcp/controlTools.js +34 -1
  151. package/dist/mcp/deps.d.ts +9 -0
  152. package/dist/mcp/evalTools.js +684 -21
  153. package/dist/mcp/memoryHostTools.d.ts +3 -0
  154. package/dist/mcp/memoryHostTools.js +72 -0
  155. package/dist/mcp/pipelineAuthorTools.js +14 -1
  156. package/dist/mcp/server.js +5 -0
  157. package/dist/mcp/tools.js +6 -0
  158. package/dist/mcp/triggerTools.js +77 -31
  159. package/dist/operatorAgent/evalGate.d.ts +22 -0
  160. package/dist/operatorAgent/evalGate.js +77 -0
  161. package/dist/operatorAgent/fakeHost.js +10 -2
  162. package/dist/operatorAgent/host.js +18 -2
  163. package/dist/operatorAgent/index.d.ts +1 -0
  164. package/dist/operatorAgent/index.js +1 -0
  165. package/dist/operatorAgent/internalAgents.d.ts +11 -0
  166. package/dist/operatorAgent/internalAgents.js +13 -0
  167. package/dist/operatorAgent/piModel.d.ts +5 -1
  168. package/dist/operatorAgent/piModel.js +15 -10
  169. package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
  170. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
  171. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
  172. package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
  173. package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
  174. package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
  175. package/dist/operatorAgent/profiles/guideTools.js +123 -0
  176. package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
  177. package/dist/operatorAgent/profiles/stageflowGuide.js +132 -0
  178. package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
  179. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  180. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
  181. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
  182. package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
  183. package/dist/operatorAgent/types.d.ts +23 -0
  184. package/dist/package-meta.d.ts +1 -1
  185. package/dist/package-meta.js +1 -1
  186. package/dist/runstore/retention.d.ts +2 -1
  187. package/dist/runstore/retention.js +3 -7
  188. package/dist/runstore/runStatusClock.d.ts +17 -0
  189. package/dist/runstore/runStatusClock.js +19 -0
  190. package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
  191. package/dist/runstore/sqlite/SqliteRunStore.js +45 -45
  192. package/dist/runstore/workspaceLayout.js +6 -1
  193. package/dist/runtime/artifactDiskInspect.d.ts +14 -0
  194. package/dist/runtime/artifactDiskInspect.js +52 -0
  195. package/dist/runtime/artifactPath.d.ts +24 -0
  196. package/dist/runtime/artifactPath.js +84 -0
  197. package/dist/runtime/completionCheckRunner.js +15 -43
  198. package/dist/runtime/githubPollSource.d.ts +22 -4
  199. package/dist/runtime/githubPollSource.js +93 -8
  200. package/dist/runtime/priorArtifactGrants.d.ts +8 -0
  201. package/dist/runtime/priorArtifactGrants.js +105 -0
  202. package/dist/runtime/runActionEligibility.d.ts +46 -0
  203. package/dist/runtime/runActionEligibility.js +123 -0
  204. package/dist/runtime/runManager.d.ts +4 -1
  205. package/dist/runtime/runManager.js +52 -10
  206. package/dist/runtime/runRetryCoordinator.d.ts +2 -14
  207. package/dist/runtime/runRetryCoordinator.js +3 -67
  208. package/dist/runtime/sandboxContainer.d.ts +7 -0
  209. package/dist/runtime/sandboxContainer.js +48 -6
  210. package/dist/runtime/sandboxDesk.d.ts +2 -1
  211. package/dist/runtime/sandboxDesk.js +11 -12
  212. package/dist/runtime/stageEnvironment.js +1 -0
  213. package/dist/runtime/stageRoots.d.ts +12 -0
  214. package/dist/runtime/stageSandbox.d.ts +2 -1
  215. package/dist/runtime/stageSandbox.js +18 -2
  216. package/dist/runtime/stageWorker.js +20 -0
  217. package/dist/server/agentAuthorRoutes.js +12 -3
  218. package/dist/server/bootstrap.d.ts +5 -0
  219. package/dist/server/bootstrap.js +14 -1
  220. package/dist/server/evalRoutes.js +38 -9
  221. package/dist/server/guideRoutes.d.ts +5 -0
  222. package/dist/server/guideRoutes.js +166 -0
  223. package/dist/server/http.d.ts +10 -0
  224. package/dist/server/http.js +35 -1
  225. package/dist/server/internalAgentRoutes.d.ts +6 -0
  226. package/dist/server/internalAgentRoutes.js +12 -0
  227. package/dist/server/mcpHost.js +2 -0
  228. package/dist/toolkit/meta.d.ts +1 -1
  229. package/dist/toolkit/meta.js +39 -1
  230. package/dist/toolkit/profiles.d.ts +5 -0
  231. package/dist/toolkit/profiles.js +12 -0
  232. package/dist/tools/askOperator.js +2 -1
  233. package/dist/tools/emitStageEnvelope.d.ts +17 -16
  234. package/dist/tools/emitStageEnvelope.js +46 -32
  235. package/dist/tools/stageToolNames.d.ts +4 -0
  236. package/dist/tools/stageToolNames.js +4 -0
  237. package/dist/tools/workingState.d.ts +40 -0
  238. package/dist/tools/workingState.js +108 -0
  239. package/dist/tools/writeStageArtifact.d.ts +0 -9
  240. package/dist/tools/writeStageArtifact.js +2 -1
  241. package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-S5C_vemc.js} +2 -2
  242. package/dist/ui/assets/adapters-C0l4ruiR.js +17 -0
  243. package/dist/ui/assets/chat-CjnsmvqR.js +2 -0
  244. package/dist/ui/assets/chat-DFDd4UjS.css +2 -0
  245. package/dist/ui/assets/{main-CHlmTNJe.css → main-Del1GeLf.css} +1 -1
  246. package/dist/ui/assets/main-_yXVqcW9.js +277 -0
  247. package/dist/ui/chat.html +3 -4
  248. package/dist/ui/index.html +3 -4
  249. package/dist/workshop/chatTurn.js +1 -1
  250. package/dist/workshop/workshopPin.d.ts +3 -3
  251. package/dist/workshop/workshopPin.js +16 -15
  252. package/examples/compaction-stress/README.md +30 -0
  253. package/examples/compaction-stress/check-run.mjs +53 -0
  254. package/examples/compaction-stress/collect-evidence.yaml +60 -0
  255. package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
  256. package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
  257. package/examples/compaction-stress/generate-evidence.mjs +110 -0
  258. package/examples/compaction-stress/root-cause.yaml +26 -0
  259. package/examples/compaction-stress/stageflow.yaml +9 -0
  260. package/examples/compaction-stress/write-postmortem.yaml +40 -0
  261. package/examples/evals/README.md +18 -0
  262. package/examples/evals/guide-behavior.dataset.jsonl +15 -0
  263. package/examples/evals/guide-behavior.eval.yaml +35 -0
  264. package/examples/evals/guide-depth.dataset.jsonl +35 -0
  265. package/examples/evals/guide-depth.eval.yaml +169 -0
  266. package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
  267. package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
  268. package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
  269. package/examples/memory/evals/guide-depth.eval.yaml +169 -0
  270. package/package.json +1 -1
  271. package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
  272. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
  273. package/dist/ui/assets/chat-Df_wCpak.js +0 -5
  274. package/dist/ui/assets/chat-P7KQznHm.css +0 -2
  275. package/dist/ui/assets/main-CvVfxmMd.js +0 -271
@@ -1,3 +1,4 @@
1
+ import { Type } from "typebox";
1
2
  import { parse as parseYaml } from "yaml";
2
3
  import { peekDefaultModel } from "../../models/catalog.js";
3
4
  import { validateDraftPackage, } from "../../config/draftPackage.js";
@@ -157,9 +158,13 @@ export function buildTaskAddProposal(draft, userText) {
157
158
  affectedStageIds: [],
158
159
  };
159
160
  }
161
+ const closed = { additionalProperties: false };
162
+ const jsonObject = Type.Record(Type.String(), Type.Unknown());
163
+ const emptyParams = Type.Object({}, closed);
160
164
  const readDraftTool = {
161
165
  name: "read_draft",
162
166
  description: "Read the current Workshop virtual draft package",
167
+ parameters: emptyParams,
163
168
  handler(_args, ctx) {
164
169
  return {
165
170
  ok: true,
@@ -199,6 +204,18 @@ function coercePlainObject(value) {
199
204
  const proposeDraftTool = {
200
205
  name: "propose_draft",
201
206
  description: "Legacy bulk draft replace. Prefer create_*/edit_* tools. Applies immediately (Accept confirms; Reject soft-undos when unchanged).",
207
+ parameters: Type.Union([
208
+ Type.Object({
209
+ draft: jsonObject,
210
+ summary: Type.Optional(Type.String()),
211
+ }, closed),
212
+ Type.Object({
213
+ pipeline: jsonObject,
214
+ stages: Type.Optional(Type.Array(Type.Unknown())),
215
+ task: Type.Optional(jsonObject),
216
+ summary: Type.Optional(Type.String()),
217
+ }, closed),
218
+ ]),
202
219
  handler(args, ctx) {
203
220
  const summary = typeof args.summary === "string" && args.summary.trim()
204
221
  ? args.summary.trim()
@@ -244,6 +261,7 @@ const proposeDraftTool = {
244
261
  const validateDraftTool = {
245
262
  name: "validate_draft",
246
263
  description: "Run catalog validation against the current draft",
264
+ parameters: emptyParams,
247
265
  async handler(_args, ctx) {
248
266
  const draft = readDraftFromContext(ctx.getContext());
249
267
  const result = await validateDraftPackage(draft, { strict: true });
@@ -254,6 +272,11 @@ function createRetrieveDocsTool(retriever) {
254
272
  return {
255
273
  name: "retrieve_docs",
256
274
  description: "Retrieve current public Stageflow docs/examples snippets by query. Falls back gracefully when retrieval fails — keep using the baked playbook.",
275
+ parameters: Type.Object({
276
+ query: Type.String({ minLength: 1 }),
277
+ kind: Type.Optional(Type.Union([Type.Literal("docs"), Type.Literal("examples"), Type.Literal("any")])),
278
+ limit: Type.Optional(Type.Number()),
279
+ }, closed),
257
280
  async handler(args) {
258
281
  const query = typeof args.query === "string" && args.query.trim()
259
282
  ? args.query.trim()
@@ -3,4 +3,4 @@
3
3
  * guidance and public YAML catalog concepts. First-class Workshop editors
4
4
  * cover the core path; advanced topics are explain-only.
5
5
  */
6
- export declare const WORKSHOP_AUTHOR_PLAYBOOK = "You are the Stageflow Workshop Author: a colleague designing a workflow with the operator. You are not a task runner and not a documentation bot.\n\n## How you talk\n- Sound like a working conversation. Short turns, plain words, present tense.\n- Default length is one to four sentences. No headings, no outlines, no catalog recitation, no multi-step plans unless they ask for a plan or a deeper explanation.\n- One move per turn: reflect what you heard, ask the one question that changes the draft, or make a small edit and say what you assumed.\n- Mirror their words. Refer back to decisions already made. If they correct you, drop the old assumption and continue.\n- Do not narrate tools (\"I'll call create_stage\"). The studio shows the draft. Say what changed in plain language.\n- The reference below is private. Use it when you edit. Do not recite it.\n\n## When you ask, when you edit\n- Clarify before you create when a missing detail would change the stages, the wiring, the goal, or a gate. Do not invent that detail.\n- Ask the single most useful question. Add a second only when both answers block the next edit and they don't depend on each other.\n- Make the question specific (\"Should review block shipping until someone accepts?\") rather than open (\"Tell me more\").\n- Skip questions you can default: names, ordinary linear order, a pipeline model when one is already set. Say the assumption in the same breath as the edit.\n- When the request is already specific, edit. Do not ask permission to do what they just asked.\n- Ask before a second pipeline when one is already open. If the answer is still ambiguous, ask once more.\n- When nothing is selected, create an untitled build and focus it.\n- When the operator names an existing pipeline, focus it rather than create a new one.\n- After an edit, one sentence on what landed, then the next open question if one remains. Do not paste YAML unless they ask to see it.\n\nGood: \"A release that waits for review, or one that publishes as soon as checks pass?\"\nBad: a paragraph of pipeline theory, a numbered plan, and three questions at once.\n\n## Tools\n- Never invent a disk path; never use bash/write/edit.\n- Tools (prefer these): list_builds, focus_build, create_build, create_pipeline / edit_pipeline, create_stage / edit_stage, create_task / edit_task, read_draft, validate_draft, save. propose_draft is legacy bulk replace \u2014 avoid unless the operator asks for a full replace. Do not wait for Accept before mutating; propose\u2192Accept gating is superseded by immediate mutate + soft undo.\n- list_builds returns the same rows as the studio and does not create a build; use catalogListed before telling the operator a pipeline or task appears on Tasks or Pipelines. focus_build moves this chat onto a build id, or onto a disk pipeline by project root and path. create_build persists a new untitled build and focuses it, including when another build is already open. Do not refuse create_build.\n- Build tie (projectRoot + relativePath on a row) is the last saved catalog anchor for that build id \u2014 not every path on disk counts for Tasks (e.g. image package roots or paths outside stageflow.yaml scan). When the operator asks what is on disk for Run/Tasks, cite the last successful save tool result (published.project_root_path, published.pipeline_path, published.task_path) or a catalogListed row. Draft-only task changes are not on disk until save returns task_path.\n- Mutations apply to the in-memory draft immediately (studio updates before Accept). Accept confirms; Reject soft-undos that mutation when the draft fingerprint is unchanged. Soft undo does not reverse a successful save to disk.\n- Chat History/New threads persist under workshop/sessions (transcript + session id + activeBuildId). The draft persists as a build under workshop/builds and is not embedded in the session blob. The studio picker lists open builds and on-disk pipelines.\n- Always read_draft before mutating. Prefer uses: ./id.yaml file-backed stages and Author dialect (io / verify / on_verify_fail, route / entry).\n- Validate with validate_draft; fix hard errors. Warnings like pipeline.model_applies or pipeline.route_all_gated may be intentional.\n- save writes catalog YAML only when the operator asks to persist. If they do not name a folder, omit directory (the host writes workshop/<pipeline-id> and registers that folder so Run lists the pipeline and task). If they name a folder, pass that directory. Do not ask for a path before saving. validate-then-write; allowInvalid only if they explicitly request saving invalid YAML. After save, use published.* paths in the tool result \u2014 not list_builds tie alone \u2014 to answer where files landed and whether a task file was written.\n- Stages are domain-agnostic configurable workflows (releases, research, ops, SDLC, \u2026) \u2014 not product stage types.\n- Use retrieve_docs when the operator asks for current docs/examples or when refining against public guidance. If retrieval fails, continue with this playbook alone.\n- Core path = first-class Workshop editors. Advanced topics below are explain-only \u2014 do not invent Workshop editors for them.\n\n## Core: DAG / linear stages\n- Pipeline needs id + non-empty stages. Set model on pipeline (or stage / stageflow.yaml); empty at all tiers fails load.\n- Wire on the source only: entry: true on \u22651 root; forward route: [{ to }]. Never author needs, fork, top-level feedback_loop, route_select, or allow_none.\n- Linear A\u2192B; fan-out = multiple to: (all run); Join = several parents to: the same child; exclusive branch = mutually exclusive ifs on required payload fields (agent emits a discriminator, not successor ids).\n- if: leaf {field,op,value} or all/any/not; ops eq|ne|gt|gte|lt|lte|in|not_in. Path segments must be required in the producer io.output.schema.\n- on: is skip-cascade policy only (default succeeded); it does not launch from failed/skipped.\n- Consumer io.input \u2286 each parent's io.output. Prefer external stages when shared or larger than a tiny draft.\n\n## Core: prompts\n- Required system_prompt. Last tool call of an attempt is emit_stage_envelope (or ask_operator while waiting on HITL).\n- Optional skill: belongs on the stage body (create_stage / edit_stage body or stage file) \u2014 never on a uses: wrapper.\n- Name required io.output.schema fields in the prompt. summary/payload carry outcomes and artifact pointers only.\n- Land required files with write_stage_artifact (attempt dir). Checkout edits are not after-phase artifact proof.\n- Gated sequence: write artifact \u2192 ask_operator \u2192 emit only after accept; revise on reject in the same attempt.\n- Prefer a configured provider model; siblings after fan-out share a family unless the operator asks otherwise.\n\n## Core: io / verify / on_verify_fail\n- Always author both io.input.schema and io.output.schema (object roots). Success payload is validated on emit.\n- Write Author keys only \u2014 not legacy payload_schema / pre_emit_checks / completion / recovery (those are IR after load).\n- verify lives on the stage body; when: [emit] | [after] | both. Defaults: gate\u2192emit; command/checkout_changes/checklist/payload_schema\u2192after; artifact must set when.\n- Types: gate (+ kind), artifact, command, checklist, payload_schema, checkout_changes (+ path_fields).\n- Emit-phase fail = soft reject (retry same turn). After-phase = verified stage execution hard proof.\n- on_verify_fail on the pipeline entry (beside uses:); needs \u22651 after-phase verify. Prefer repair (idempotent, max_attempts) vs manual (side-effecting publish).\n\n## Core: HITL (ask_operator)\n- gate_kinds omit/[] \u2192 tool off; non-empty allowlist \u2192 tool on (confirm, free_text, artifact_backed, multi_question).\n- Load-bearing gate: body verify type: gate (default emit) \u2014 success emit needs a matching accept / answered exchange.\n- ask_operator does not complete the stage \u2014 only emit_stage_envelope does.\n- artifact_backed paths come from write_stage_artifact (run-relative), not checkout paths.\n\n## Core: artifacts / envelopes\n- Envelope: status, non-empty summary, artifacts (may be []), success payload vs io.output.schema.\n- One advancing emit per attempt. Downstream reads envelopes, not chat. Fan-in Join gets priorEnvelopesByStage.\n- Catalog routing \u2260 agent fork_choice. Rejected: envelope clone_forks.\n\n## Optional task\n- A task (id + goal) may be attached or created for the package; pipeline-only drafts remain valid without one.\n\n## Explain-only (no first-class Workshop editors)\n\n### Clone Chain\nSealed emitter \u2192 clone child \u2192 Join. One named $ref array on the emitter; the child's whole input is that $ref. clone_cap + clone_mode on the emitter pipeline entry only. Empty/over-cap fails emitter emit. Never author clonable, clone_forks, or emit-time skip|once|fanout. Point operators at docs/yaml-catalog Clone Chain and examples/feature-loop.\n\n### Feedback loops\n{ type: loop, to, max_replays, on_max_replays, replay_session } inside source route (forward DAG stays acyclic). Success emit must include envelope feedback_loop: continue | send_back. replay_safe: false blocks replay. Prefer a forward review \u2192 address-feedback \u2192 approve when a separate fix stage is clearer. See examples/feedback-loop.\n\n### Stage MCP\nmcp: [names] on the pipeline entry; project-root .mcp.json; curated stage env interpolation; reserved name stageflow; builtins always available. Validate checks shape/names/PATH; connect/env fail at run. Explain and retrieve docs/examples (e.g. examples/stage-mcp); do not offer a Workshop MCP editor.\n\n### Complex recovery\nBeyond basic repair/manual: operator sf runs recover / MCP recover_manual_stage; after-phase gates/artifacts are history- and disk-aware. Point at verified-stage-execution docs; do not offer Workshop recovery editors.\n\n## Anti-patterns\n- Never needs / fork / top-level feedback_loop \u2192 use route / { type: loop }.\n- Never legacy contract keys \u2192 use io / verify / on_verify_fail.\n- Never omit io or leave model empty at all tiers \u2192 both schemas; pipeline model minimum.\n- Never emit before HITL accept \u2192 verify gate + prompt sequence.\n- Never ambient Host secrets \u2192 stage secrets:.\n- Never clonable / clone_forks \u2192 sealed Clone Chain.\n\nReview-with-blockers: wire review \u2192 address-feedback \u2192 approve/ship; put verdict / blocking_findings in review io.output.schema.\n";
6
+ export declare const WORKSHOP_AUTHOR_PLAYBOOK = "You are the Stageflow Workshop Author: a colleague designing a workflow with the operator. You are not a task runner and not a documentation bot.\n\n## How you talk\n- Sound like a working conversation. Short turns, plain words, present tense.\n- Default length is one to four sentences. No headings, no outlines, no catalog recitation, no multi-step plans unless they ask for a plan or a deeper explanation.\n- One move per turn: reflect what you heard, ask the one question that changes the draft, or make a small edit and say what you assumed.\n- Mirror their words. Refer back to decisions already made. If they correct you, drop the old assumption and continue.\n- Do not narrate tools (\"I'll call create_stage\"). The studio shows the draft. Say what changed in plain language.\n- The reference below is private. Use it when you edit. Do not recite it.\n\n## When you ask, when you edit\n- Clarify before you create when a missing detail would change the stages, the wiring, the goal, or a gate. Do not invent that detail.\n- Ask the single most useful question. Add a second only when both answers block the next edit and they don't depend on each other.\n- Make the question specific (\"Should review block shipping until someone accepts?\") rather than open (\"Tell me more\").\n- Skip questions you can default: names, ordinary linear order, a pipeline model when one is already set. Say the assumption in the same breath as the edit.\n- When the request is already specific, edit. Do not ask permission to do what they just asked.\n- Ask before a second pipeline when one is already open. If the answer is still ambiguous, ask once more.\n- When nothing is selected, create an untitled build and focus it.\n- When the operator names an existing pipeline, focus it rather than create a new one.\n- The pipeline already open in the studio is the one to edit. read_draft returns that draft. Do not ask which pipeline, and do not pick a different row from list_builds.\n- After an edit, one sentence on what landed, then the next open question if one remains. Do not paste YAML unless they ask to see it.\n\nGood: \"A release that waits for review, or one that publishes as soon as checks pass?\"\nBad: a paragraph of pipeline theory, a numbered plan, and three questions at once.\n\n## Tools\n- Never invent a disk path; never use bash/write/edit.\n- Tools (prefer these): list_builds, focus_build, create_build, create_pipeline / edit_pipeline, create_stage / edit_stage, create_task / edit_task, read_draft, validate_draft, save. propose_draft is legacy bulk replace \u2014 avoid unless the operator asks for a full replace. Do not wait for Accept before mutating; propose\u2192Accept gating is superseded by immediate mutate + soft undo.\n- list_builds returns the same rows as the studio and does not create a build; use catalogListed before telling the operator a pipeline or task appears on Tasks or Pipelines. focus_build moves this chat onto a build id, or onto a disk pipeline by project root and path (mid-turn, the mutation pin moves with it). create_build persists a new untitled build only when none is pinned; if a build is already pinned it returns that build. edit_pipeline stages wiring must match existing stage bodies \u2014 use create_stage / edit_stage to add or remove stages.\n- Build tie (projectRoot + relativePath on a row) is the last saved catalog anchor for that build id \u2014 not every path on disk counts for Tasks (e.g. image package roots or paths outside stageflow.yaml scan). When the operator asks what is on disk for Run/Tasks, cite the last successful save tool result (published.project_root_path, published.pipeline_path, published.task_path) or a catalogListed row. Draft-only task changes are not on disk until save returns task_path.\n- Mutations apply to the in-memory draft immediately (studio updates before Accept). Accept confirms; Reject soft-undos that mutation when the draft fingerprint is unchanged. Soft undo does not reverse a successful save to disk.\n- Chat History/New threads persist under workshop/sessions (transcript + session id + activeBuildId). The draft persists as a build under workshop/builds and is not embedded in the session blob. The studio picker lists open builds and on-disk pipelines.\n- Always read_draft before mutating. Prefer uses: ./id.yaml file-backed stages and Author dialect (io / verify / on_verify_fail, route / entry).\n- Validate with validate_draft; fix hard errors. Warnings like pipeline.model_applies or pipeline.route_all_gated may be intentional.\n- save writes catalog YAML only when the operator asks to persist. If they do not name a folder, omit directory (the host writes workshop/<pipeline-id> and registers that folder so Run lists the pipeline and task). If they name a folder, pass that directory. Do not ask for a path before saving. validate-then-write; allowInvalid only if they explicitly request saving invalid YAML. After save, use published.* paths in the tool result \u2014 not list_builds tie alone \u2014 to answer where files landed and whether a task file was written.\n- Stages are domain-agnostic configurable workflows (releases, research, ops, SDLC, \u2026) \u2014 not product stage types.\n- Use retrieve_docs when the operator asks for current docs/examples or when refining against public guidance. If retrieval fails, continue with this playbook alone.\n- Core path = first-class Workshop editors. Advanced topics below are explain-only \u2014 do not invent Workshop editors for them.\n\n## Core: DAG / linear stages\n- Pipeline needs id + non-empty stages. Set model on pipeline (or stage / stageflow.yaml); empty at all tiers fails load.\n- Wire on the source only: entry: true on \u22651 root; forward route: [{ to }]. Never author needs, fork, top-level feedback_loop, route_select, or allow_none.\n- Linear A\u2192B; fan-out = multiple to: (all run); Join = several parents to: the same child; exclusive branch = mutually exclusive ifs on required payload fields (agent emits a discriminator, not successor ids).\n- if: leaf {field,op,value} or all/any/not; ops eq|ne|gt|gte|lt|lte|in|not_in. Path segments must be required in the producer io.output.schema.\n- on: is skip-cascade policy only (default succeeded); it does not launch from failed/skipped.\n- Consumer io.input \u2286 each parent's io.output. Prefer external stages when shared or larger than a tiny draft.\n\n## Core: prompts\n- Required system_prompt. Last tool call of an attempt is emit_stage_envelope (or ask_operator while waiting on HITL).\n- Optional skill: belongs on the stage body (create_stage / edit_stage body or stage file) \u2014 never on a uses: wrapper.\n- Name required io.output.schema fields in the prompt. summary/payload carry outcomes and artifact pointers only.\n- Land required files with write_stage_artifact (attempt dir). Checkout edits are not after-phase artifact proof.\n- Under sandbox: the stage assignment lists each prior file at /workspace/grants/<stage>/\u2026 (that path is the file). Consumer prompts read those paths or payload fields, never stages/<id>/attempts/\u2026/artifacts host paths; /workspace/work is scratch only.\n- Gated sequence: write artifact \u2192 ask_operator \u2192 emit only after accept; revise on reject in the same attempt.\n- Prefer a configured provider model; siblings after fan-out share a family unless the operator asks otherwise.\n\n## Core: io / verify / on_verify_fail\n- Always author both io.input.schema and io.output.schema (object roots). Success payload is validated on emit.\n- Write Author keys only \u2014 not legacy payload_schema / pre_emit_checks / completion / recovery (those are IR after load).\n- verify lives on the stage body; when: [emit] | [after] | both. Defaults: gate\u2192emit; command/checkout_changes/checklist/payload_schema\u2192after; artifact must set when.\n- Types: gate (+ kind), artifact, command, checklist, payload_schema, checkout_changes (+ path_fields).\n- Emit-phase fail = soft reject (retry same turn). After-phase = verified stage execution hard proof.\n- on_verify_fail on the pipeline entry (beside uses:); needs \u22651 after-phase verify. Prefer repair (idempotent, max_attempts) vs manual (side-effecting publish).\n\n## Core: HITL (ask_operator)\n- gate_kinds omit/[] \u2192 tool off; non-empty allowlist \u2192 tool on (confirm, free_text, artifact_backed, multi_question).\n- Load-bearing gate: body verify type: gate (default emit) \u2014 success emit needs a matching accept / answered exchange.\n- ask_operator does not complete the stage \u2014 only emit_stage_envelope does.\n- artifact_backed paths come from write_stage_artifact (run-relative), not checkout paths.\n\n## Core: artifacts / envelopes\n- Artifact contract: durable writes only via write_stage_artifact; emit artifacts[] \u2260 disk until emit disk-truth accepts each listed path under this attempt; empty [] is fine.\n- Facts in payload (required io.output fields). Files via write_stage_artifact then list returned run-relative paths on emit.\n- Sandbox next stage: read the granted file at /workspace/grants/<stage>/\u2026 from the assignment, or payload \u2014 never stages/.../artifacts host paths; never treat /workspace/work as handoff.\n- Do not inject after-phase verify type: artifact on every create_stage; add only when an explicit basename needs hard proof.\n- Envelope: status, non-empty summary, artifacts (may be []), success payload vs io.output.schema.\n- One advancing emit per attempt. Downstream reads envelopes, not chat. Fan-in Join gets priorEnvelopesByStage.\n- Catalog routing \u2260 agent fork_choice. Rejected: envelope clone_forks.\n\n## Optional task\n- A task (id + goal) may be attached or created for the package; pipeline-only drafts remain valid without one.\n\n## Explain-only (no first-class Workshop editors)\n\n### Clone Chain\nSealed emitter \u2192 clone child \u2192 Join. One named $ref array on the emitter; the child's whole input is that $ref. clone_cap + clone_mode on the emitter pipeline entry only. Empty/over-cap fails emitter emit. Never author clonable, clone_forks, or emit-time skip|once|fanout. Point operators at docs/yaml-catalog Clone Chain and examples/feature-loop.\n\n### Feedback loops\n{ type: loop, to, max_replays, on_max_replays, replay_session } inside source route (forward DAG stays acyclic). Success emit must include envelope feedback_loop: continue | send_back. replay_safe: false blocks replay. Prefer a forward review \u2192 address-feedback \u2192 approve when a separate fix stage is clearer. See examples/feedback-loop.\n\n### Stage MCP\nmcp: [names] on the pipeline entry; project-root .mcp.json; curated stage env interpolation; reserved name stageflow; builtins always available. Validate checks shape/names/PATH; connect/env fail at run. Explain and retrieve docs/examples (e.g. examples/stage-mcp); do not offer a Workshop MCP editor.\n\n### Complex recovery\nBeyond basic repair/manual: operator sf runs recover / MCP recover_manual_stage; after-phase gates/artifacts are history- and disk-aware. Point at verified-stage-execution docs; do not offer Workshop recovery editors.\n\n## Anti-patterns\n- Never needs / fork / top-level feedback_loop \u2192 use route / { type: loop }.\n- Never legacy contract keys \u2192 use io / verify / on_verify_fail.\n- Never omit io or leave model empty at all tiers \u2192 both schemas; pipeline model minimum.\n- Never emit before HITL accept \u2192 verify gate + prompt sequence.\n- Never ambient Host secrets \u2192 stage secrets:.\n- Never clonable / clone_forks \u2192 sealed Clone Chain.\n- Never cross-stage handoff via stages/.../artifacts host paths or /workspace/work under sandbox \u2192 write_stage_artifact + grants/payload.\n- Never blanket after-phase artifact verify on every create_stage.\n\nReview-with-blockers: wire review \u2192 address-feedback \u2192 approve/ship; put verdict / blocking_findings in review io.output.schema.\n";
@@ -22,6 +22,7 @@ export const WORKSHOP_AUTHOR_PLAYBOOK = `You are the Stageflow Workshop Author:
22
22
  - Ask before a second pipeline when one is already open. If the answer is still ambiguous, ask once more.
23
23
  - When nothing is selected, create an untitled build and focus it.
24
24
  - When the operator names an existing pipeline, focus it rather than create a new one.
25
+ - The pipeline already open in the studio is the one to edit. read_draft returns that draft. Do not ask which pipeline, and do not pick a different row from list_builds.
25
26
  - After an edit, one sentence on what landed, then the next open question if one remains. Do not paste YAML unless they ask to see it.
26
27
 
27
28
  Good: "A release that waits for review, or one that publishes as soon as checks pass?"
@@ -30,7 +31,7 @@ Bad: a paragraph of pipeline theory, a numbered plan, and three questions at onc
30
31
  ## Tools
31
32
  - Never invent a disk path; never use bash/write/edit.
32
33
  - Tools (prefer these): list_builds, focus_build, create_build, create_pipeline / edit_pipeline, create_stage / edit_stage, create_task / edit_task, read_draft, validate_draft, save. propose_draft is legacy bulk replace — avoid unless the operator asks for a full replace. Do not wait for Accept before mutating; propose→Accept gating is superseded by immediate mutate + soft undo.
33
- - list_builds returns the same rows as the studio and does not create a build; use catalogListed before telling the operator a pipeline or task appears on Tasks or Pipelines. focus_build moves this chat onto a build id, or onto a disk pipeline by project root and path. create_build persists a new untitled build and focuses it, including when another build is already open. Do not refuse create_build.
34
+ - list_builds returns the same rows as the studio and does not create a build; use catalogListed before telling the operator a pipeline or task appears on Tasks or Pipelines. focus_build moves this chat onto a build id, or onto a disk pipeline by project root and path (mid-turn, the mutation pin moves with it). create_build persists a new untitled build only when none is pinned; if a build is already pinned it returns that build. edit_pipeline stages wiring must match existing stage bodies — use create_stage / edit_stage to add or remove stages.
34
35
  - Build tie (projectRoot + relativePath on a row) is the last saved catalog anchor for that build id — not every path on disk counts for Tasks (e.g. image package roots or paths outside stageflow.yaml scan). When the operator asks what is on disk for Run/Tasks, cite the last successful save tool result (published.project_root_path, published.pipeline_path, published.task_path) or a catalogListed row. Draft-only task changes are not on disk until save returns task_path.
35
36
  - Mutations apply to the in-memory draft immediately (studio updates before Accept). Accept confirms; Reject soft-undos that mutation when the draft fingerprint is unchanged. Soft undo does not reverse a successful save to disk.
36
37
  - Chat History/New threads persist under workshop/sessions (transcript + session id + activeBuildId). The draft persists as a build under workshop/builds and is not embedded in the session blob. The studio picker lists open builds and on-disk pipelines.
@@ -54,6 +55,7 @@ Bad: a paragraph of pipeline theory, a numbered plan, and three questions at onc
54
55
  - Optional skill: belongs on the stage body (create_stage / edit_stage body or stage file) — never on a uses: wrapper.
55
56
  - Name required io.output.schema fields in the prompt. summary/payload carry outcomes and artifact pointers only.
56
57
  - Land required files with write_stage_artifact (attempt dir). Checkout edits are not after-phase artifact proof.
58
+ - Under sandbox: the stage assignment lists each prior file at /workspace/grants/<stage>/… (that path is the file). Consumer prompts read those paths or payload fields, never stages/<id>/attempts/…/artifacts host paths; /workspace/work is scratch only.
57
59
  - Gated sequence: write artifact → ask_operator → emit only after accept; revise on reject in the same attempt.
58
60
  - Prefer a configured provider model; siblings after fan-out share a family unless the operator asks otherwise.
59
61
 
@@ -72,6 +74,10 @@ Bad: a paragraph of pipeline theory, a numbered plan, and three questions at onc
72
74
  - artifact_backed paths come from write_stage_artifact (run-relative), not checkout paths.
73
75
 
74
76
  ## Core: artifacts / envelopes
77
+ - Artifact contract: durable writes only via write_stage_artifact; emit artifacts[] ≠ disk until emit disk-truth accepts each listed path under this attempt; empty [] is fine.
78
+ - Facts in payload (required io.output fields). Files via write_stage_artifact then list returned run-relative paths on emit.
79
+ - Sandbox next stage: read the granted file at /workspace/grants/<stage>/… from the assignment, or payload — never stages/.../artifacts host paths; never treat /workspace/work as handoff.
80
+ - Do not inject after-phase verify type: artifact on every create_stage; add only when an explicit basename needs hard proof.
75
81
  - Envelope: status, non-empty summary, artifacts (may be []), success payload vs io.output.schema.
76
82
  - One advancing emit per attempt. Downstream reads envelopes, not chat. Fan-in Join gets priorEnvelopesByStage.
77
83
  - Catalog routing ≠ agent fork_choice. Rejected: envelope clone_forks.
@@ -100,6 +106,8 @@ Beyond basic repair/manual: operator sf runs recover / MCP recover_manual_stage;
100
106
  - Never emit before HITL accept → verify gate + prompt sequence.
101
107
  - Never ambient Host secrets → stage secrets:.
102
108
  - Never clonable / clone_forks → sealed Clone Chain.
109
+ - Never cross-stage handoff via stages/.../artifacts host paths or /workspace/work under sandbox → write_stage_artifact + grants/payload.
110
+ - Never blanket after-phase artifact verify on every create_stage.
103
111
 
104
112
  Review-with-blockers: wire review → address-feedback → approve/ship; put verdict / blocking_findings in review io.output.schema.
105
113
  `;
@@ -26,6 +26,8 @@ export declare function buildCreatePipelineDraft(draft: DraftPackage, args: Reco
26
26
  ok: false;
27
27
  error: string;
28
28
  };
29
+ /** Refuse stages wiring that diverges from draft.stages bodies (no silent prune). */
30
+ export declare function validateEditPipelineStagesWiring(draft: DraftPackage, nextStages: Array<Record<string, unknown>>): string | null;
29
31
  export declare function buildEditPipelineDraft(draft: DraftPackage, args: Record<string, unknown>): {
30
32
  ok: true;
31
33
  nextDraft: DraftPackage;
@@ -1,8 +1,10 @@
1
1
  import path from "node:path";
2
+ import { Type } from "typebox";
2
3
  import { peekDefaultModel } from "../../models/catalog.js";
3
4
  import { parse as parseYaml } from "yaml";
4
5
  import { CatalogPathError, resolveDefaultWriteRoot, } from "../../config/catalogRelativePath.js";
5
6
  import { defaultWorkshopPackageDirectory } from "../../config/ensureCatalogScanRoot.js";
7
+ import { canonicalizeNeedTerminalState } from "../../config/pipelineNeeds.js";
6
8
  import { publishDraftPackage } from "../../config/publishDraftPackage.js";
7
9
  import { globalStageflowHome } from "../../project/globalHome.js";
8
10
  import { emptyDraftPackage, isWorkshopDraftContext, readDraftFromContext, withDraft, } from "../draftContext.js";
@@ -34,6 +36,9 @@ export const WORKSHOP_AUTHOR_TOOL_NAMES = [
34
36
  "retrieve_docs",
35
37
  ];
36
38
  export const WORKSHOP_FORBIDDEN_AGENT_TOOLS = ["bash", "write", "edit"];
39
+ const closed = { additionalProperties: false };
40
+ const jsonObject = Type.Record(Type.String(), Type.Unknown());
41
+ const emptyParams = Type.Object({}, closed);
37
42
  const REQUIRED_IO = {
38
43
  io: {
39
44
  input: { schema: { type: "object" } },
@@ -82,6 +87,9 @@ function stringArg(args, key) {
82
87
  const value = args[key];
83
88
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
84
89
  }
90
+ function isStringList(value) {
91
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
92
+ }
85
93
  function applyMutation(ctx, summary, nextDraft) {
86
94
  const current = readDraftFromContext(ctx.getContext());
87
95
  const proposal = {
@@ -174,6 +182,52 @@ async function resolveSaveWriteTarget(ctx, options) {
174
182
  await store.close();
175
183
  }
176
184
  }
185
+ function normalizeRouteOnField(on) {
186
+ if (Array.isArray(on)) {
187
+ return on.map((item) => canonicalizeNeedTerminalState(item) ?? item);
188
+ }
189
+ const one = canonicalizeNeedTerminalState(on);
190
+ return one ?? on;
191
+ }
192
+ function normalizeRouteEntries(route) {
193
+ if (!Array.isArray(route))
194
+ return route;
195
+ return route.map((entry) => {
196
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
197
+ return entry;
198
+ const rec = entry;
199
+ if (rec.on === undefined)
200
+ return entry;
201
+ return { ...rec, on: normalizeRouteOnField(rec.on) };
202
+ });
203
+ }
204
+ function isInlinePipelineStageBody(stage) {
205
+ return stage.system_prompt !== undefined || stage.uses === undefined;
206
+ }
207
+ function normalizePipelineStagesRouteOn(stages, options) {
208
+ const next = stages.map((stage) => {
209
+ if (!stage || typeof stage !== "object")
210
+ return stage;
211
+ let out = { ...stage };
212
+ if (Array.isArray(out.route)) {
213
+ out = {
214
+ ...out,
215
+ route: normalizeRouteEntries(out.route),
216
+ };
217
+ }
218
+ if (isInlinePipelineStageBody(out) && out.io === undefined) {
219
+ out = { ...out, ...REQUIRED_IO };
220
+ }
221
+ return out;
222
+ });
223
+ if (options?.ensureEntry &&
224
+ next.length > 0 &&
225
+ !next.some((stage) => stage?.entry === true)) {
226
+ const first = next[0];
227
+ next[0] = { ...first, entry: true };
228
+ }
229
+ return next;
230
+ }
177
231
  export function buildCreateStageDraft(draft, args) {
178
232
  const existingIds = new Set(draft.pipeline.stages.map((s) => (typeof s.id === "string" ? s.id : "")));
179
233
  let stageId = stringArg(args, "id") ??
@@ -211,7 +265,7 @@ export function buildCreateStageDraft(draft, args) {
211
265
  ...(entry ? { entry: true } : {}),
212
266
  };
213
267
  if (Array.isArray(args.route)) {
214
- pipelineEntry.route = args.route;
268
+ pipelineEntry.route = normalizeRouteEntries(args.route);
215
269
  }
216
270
  const nextDraft = {
217
271
  ...draft,
@@ -244,7 +298,7 @@ export function buildEditStageDraft(draft, args) {
244
298
  if (typeof args.entry === "boolean")
245
299
  next.entry = args.entry;
246
300
  if (Array.isArray(args.route))
247
- next.route = args.route;
301
+ next.route = normalizeRouteEntries(args.route);
248
302
  if (stringArg(args, "uses"))
249
303
  next.uses = stringArg(args, "uses");
250
304
  return next;
@@ -317,7 +371,16 @@ export function buildCreatePipelineDraft(draft, args) {
317
371
  ok: true,
318
372
  nextDraft: {
319
373
  ...draft,
320
- pipeline: { ...pipeline, id: pipeline.id.trim() },
374
+ pipeline: {
375
+ ...pipeline,
376
+ id: pipeline.id.trim(),
377
+ model: typeof pipeline.model === "string" && pipeline.model.trim()
378
+ ? pipeline.model
379
+ : stringArg(args, "model") ?? peekDefaultModel(),
380
+ stages: normalizePipelineStagesRouteOn(pipeline.stages, {
381
+ ensureEntry: true,
382
+ }),
383
+ },
321
384
  ...(Array.isArray(args.stages)
322
385
  ? { stages: args.stages }
323
386
  : {}),
@@ -331,15 +394,51 @@ export function buildCreatePipelineDraft(draft, args) {
331
394
  pipeline: {
332
395
  ...draft.pipeline,
333
396
  id,
334
- ...(stringArg(args, "model") ? { model: stringArg(args, "model") } : {}),
397
+ model: stringArg(args, "model") ?? peekDefaultModel(),
335
398
  ...(args.agent !== undefined ? { agent: args.agent } : {}),
336
399
  ...(Array.isArray(args.stages)
337
- ? { stages: args.stages }
400
+ ? {
401
+ stages: normalizePipelineStagesRouteOn(args.stages, { ensureEntry: true }),
402
+ }
338
403
  : {}),
339
404
  },
340
405
  },
341
406
  };
342
407
  }
408
+ function normalizeDraftStagePath(stagePath) {
409
+ return stagePath.replace(/\\/g, "/").replace(/^\.\//, "");
410
+ }
411
+ function stageWiringMatchesArtifact(stage, artifact) {
412
+ const id = typeof stage.id === "string" ? stage.id : "";
413
+ const uses = typeof stage.uses === "string" ? stage.uses : "";
414
+ const bodyId = typeof artifact.body.id === "string" ? artifact.body.id : "";
415
+ return ((uses.length > 0 &&
416
+ normalizeDraftStagePath(artifact.path) ===
417
+ normalizeDraftStagePath(uses)) ||
418
+ (id.length > 0 && bodyId === id));
419
+ }
420
+ /** Refuse stages wiring that diverges from draft.stages bodies (no silent prune). */
421
+ export function validateEditPipelineStagesWiring(draft, nextStages) {
422
+ const artifacts = draft.stages ?? [];
423
+ for (const stage of nextStages) {
424
+ const matched = artifacts.some((artifact) => stageWiringMatchesArtifact(stage, artifact));
425
+ if (!matched) {
426
+ const label = (typeof stage.id === "string" && stage.id) ||
427
+ (typeof stage.uses === "string" && stage.uses) ||
428
+ "(unnamed)";
429
+ return `edit_pipeline stages wiring references "${label}" without a matching stage body — use create_stage / edit_stage to add or change stage files`;
430
+ }
431
+ }
432
+ for (const artifact of artifacts) {
433
+ const stillWired = nextStages.some((stage) => stageWiringMatchesArtifact(stage, artifact));
434
+ if (!stillWired) {
435
+ const label = (typeof artifact.body.id === "string" && artifact.body.id) ||
436
+ artifact.path;
437
+ return `edit_pipeline stages wiring drops stage body "${label}" — use create_stage / edit_stage to change stage files; wiring-only remove is refused`;
438
+ }
439
+ }
440
+ return null;
441
+ }
343
442
  export function buildEditPipelineDraft(draft, args) {
344
443
  const patch = isPlainObject(args.patch) ? args.patch : args;
345
444
  const nextPipeline = {
@@ -352,12 +451,30 @@ export function buildEditPipelineDraft(draft, args) {
352
451
  if (patch.agent !== undefined)
353
452
  nextPipeline.agent = patch.agent;
354
453
  if (Array.isArray(patch.stages)) {
355
- nextPipeline.stages = patch.stages;
454
+ const nextStages = normalizePipelineStagesRouteOn(patch.stages);
455
+ const wiringError = validateEditPipelineStagesWiring(draft, nextStages);
456
+ if (wiringError)
457
+ return { ok: false, error: wiringError };
458
+ nextPipeline.stages = nextStages;
356
459
  }
357
460
  if (patch.schemas !== undefined)
358
461
  nextPipeline.schemas = patch.schemas;
359
462
  if (patch.requires !== undefined)
360
463
  nextPipeline.requires = patch.requires;
464
+ if (isStringList(patch.memories)) {
465
+ if (patch.memories.length === 0)
466
+ delete nextPipeline.memories;
467
+ else
468
+ nextPipeline.memories = patch.memories;
469
+ }
470
+ if (patch.channels !== undefined) {
471
+ if (Array.isArray(patch.channels) && patch.channels.length === 0) {
472
+ delete nextPipeline.channels;
473
+ }
474
+ else {
475
+ nextPipeline.channels = patch.channels;
476
+ }
477
+ }
361
478
  return {
362
479
  ok: true,
363
480
  nextDraft: { ...draft, pipeline: nextPipeline },
@@ -428,6 +545,7 @@ export function createWorkshopBuildTools() {
428
545
  const listBuildsTool = {
429
546
  name: "list_builds",
430
547
  description: "List the same workshop rows as the studio picker (open builds and unbound disk pipelines). Each row includes catalogListed (whether Tasks/Pipelines would list that pipeline path). Does not create a build. Works when no build is focused.",
548
+ parameters: emptyParams,
431
549
  async handler(_args, ctx) {
432
550
  const live = liveWorkshopBinding(ctx);
433
551
  if (!live) {
@@ -458,7 +576,14 @@ export function createWorkshopBuildTools() {
458
576
  };
459
577
  const focusBuildTool = {
460
578
  name: "focus_build",
461
- description: "Move this chat onto a build by id, or onto an unbound disk pipeline by projectRoot and relativePath. A failed open returns the error, creates nothing, and leaves the pointer unchanged.",
579
+ description: "Move this chat onto a build by id, or onto an unbound disk pipeline by projectRoot and relativePath. Mid-turn, the mutation pin moves with the focus so studio and draft tools stay on the same build. A failed open returns the error, creates nothing, and leaves the pointer unchanged.",
580
+ parameters: Type.Union([
581
+ Type.Object({ id: Type.String({ minLength: 1 }) }, closed),
582
+ Type.Object({
583
+ projectRoot: Type.String({ minLength: 1 }),
584
+ relativePath: Type.String({ minLength: 1 }),
585
+ }, closed),
586
+ ]),
462
587
  async handler(args, ctx) {
463
588
  const live = liveWorkshopBinding(ctx);
464
589
  if (!live) {
@@ -530,7 +655,8 @@ export function createWorkshopBuildTools() {
530
655
  };
531
656
  const createBuildTool = {
532
657
  name: "create_build",
533
- description: "Persist a new untitled build and focus it, including when another build is already focused. No build id argument.",
658
+ description: "Persist a new untitled build and focus it when none is pinned. When a build is already pinned, returns that build without creating another. No build id argument.",
659
+ parameters: emptyParams,
534
660
  async handler(_args, ctx) {
535
661
  const live = liveWorkshopBinding(ctx);
536
662
  if (!live) {
@@ -551,6 +677,13 @@ export function createWorkshopAuthorMutatingTools() {
551
677
  const createPipelineTool = {
552
678
  name: "create_pipeline",
553
679
  description: "Create or replace the draft pipeline (id, optional model/stages). Mutates the in-memory draft immediately.",
680
+ parameters: Type.Object({
681
+ id: Type.String({ minLength: 1 }),
682
+ model: Type.Optional(Type.String()),
683
+ agent: Type.Optional(Type.Unknown()),
684
+ stages: Type.Optional(Type.Array(jsonObject)),
685
+ pipeline: Type.Optional(jsonObject),
686
+ }, closed),
554
687
  handler(args, ctx) {
555
688
  const built = buildCreatePipelineDraft(readDraftFromContext(ctx.getContext()), args);
556
689
  if (!built.ok)
@@ -561,7 +694,18 @@ export function createWorkshopAuthorMutatingTools() {
561
694
  };
562
695
  const editPipelineTool = {
563
696
  name: "edit_pipeline",
564
- description: "Edit pipeline fields on the current draft (id, model, stages wiring, etc.). Mutates immediately.",
697
+ description: "Edit pipeline fields on the current draft (id, model, stages wiring, etc.). Stages wiring must match existing stage bodies — use create_stage / edit_stage to add or remove stages. Mutates immediately.",
698
+ parameters: Type.Object({
699
+ id: Type.Optional(Type.String()),
700
+ model: Type.Optional(Type.String()),
701
+ agent: Type.Optional(Type.Unknown()),
702
+ stages: Type.Optional(Type.Array(jsonObject)),
703
+ schemas: Type.Optional(Type.Unknown()),
704
+ requires: Type.Optional(Type.Unknown()),
705
+ memories: Type.Optional(Type.Array(Type.String())),
706
+ channels: Type.Optional(Type.Unknown()),
707
+ patch: Type.Optional(jsonObject),
708
+ }, closed),
565
709
  handler(args, ctx) {
566
710
  const built = buildEditPipelineDraft(readDraftFromContext(ctx.getContext()), args);
567
711
  if (!built.ok)
@@ -573,6 +717,18 @@ export function createWorkshopAuthorMutatingTools() {
573
717
  const createStageTool = {
574
718
  name: "create_stage",
575
719
  description: "Add a file-backed stage to the draft (uses: ./id.yaml + stage body with io). Mutates immediately; Accept confirms, Reject soft-undos.",
720
+ parameters: Type.Object({
721
+ id: Type.Optional(Type.String()),
722
+ summary: Type.Optional(Type.String()),
723
+ label: Type.Optional(Type.String()),
724
+ uses: Type.Optional(Type.String()),
725
+ body: Type.Optional(jsonObject),
726
+ system_prompt: Type.Optional(Type.String()),
727
+ model: Type.Optional(Type.String()),
728
+ io: Type.Optional(jsonObject),
729
+ entry: Type.Optional(Type.Boolean()),
730
+ route: Type.Optional(Type.Array(Type.Unknown())),
731
+ }, closed),
576
732
  handler(args, ctx) {
577
733
  const built = buildCreateStageDraft(readDraftFromContext(ctx.getContext()), args);
578
734
  if (!built.ok)
@@ -584,6 +740,18 @@ export function createWorkshopAuthorMutatingTools() {
584
740
  const editStageTool = {
585
741
  name: "edit_stage",
586
742
  description: "Edit an existing draft stage by id (body fields and/or pipeline entry route/entry/uses). Mutates immediately.",
743
+ parameters: Type.Object({
744
+ id: Type.String({ minLength: 1 }),
745
+ patch: Type.Optional(jsonObject),
746
+ body: Type.Optional(jsonObject),
747
+ pipeline_entry: Type.Optional(jsonObject),
748
+ entry: Type.Optional(Type.Boolean()),
749
+ route: Type.Optional(Type.Array(Type.Unknown())),
750
+ uses: Type.Optional(Type.String()),
751
+ system_prompt: Type.Optional(Type.String()),
752
+ model: Type.Optional(Type.String()),
753
+ io: Type.Optional(jsonObject),
754
+ }, closed),
587
755
  handler(args, ctx) {
588
756
  const built = buildEditStageDraft(readDraftFromContext(ctx.getContext()), args);
589
757
  if (!built.ok)
@@ -595,6 +763,13 @@ export function createWorkshopAuthorMutatingTools() {
595
763
  const createTaskTool = {
596
764
  name: "create_task",
597
765
  description: "Create or replace the optional task (id + goal) on the draft. Mutates immediately.",
766
+ parameters: Type.Object({
767
+ id: Type.Optional(Type.String()),
768
+ goal: Type.Optional(Type.String()),
769
+ summary: Type.Optional(Type.String()),
770
+ filename: Type.Optional(Type.String()),
771
+ body: Type.Optional(jsonObject),
772
+ }, closed),
598
773
  handler(args, ctx) {
599
774
  const built = buildCreateTaskDraft(readDraftFromContext(ctx.getContext()), args);
600
775
  if (!built.ok)
@@ -606,6 +781,13 @@ export function createWorkshopAuthorMutatingTools() {
606
781
  const editTaskTool = {
607
782
  name: "edit_task",
608
783
  description: "Edit the draft task fields (id, goal, body). Mutates immediately.",
784
+ parameters: Type.Object({
785
+ id: Type.Optional(Type.String()),
786
+ goal: Type.Optional(Type.String()),
787
+ filename: Type.Optional(Type.String()),
788
+ patch: Type.Optional(jsonObject),
789
+ body: Type.Optional(jsonObject),
790
+ }, closed),
609
791
  handler(args, ctx) {
610
792
  const built = buildEditTaskDraft(readDraftFromContext(ctx.getContext()), args);
611
793
  if (!built.ok)
@@ -627,6 +809,12 @@ export function createSaveTool(options = {}) {
627
809
  return {
628
810
  name: "save",
629
811
  description: "Validate-then-write the draft via catalog facades (createDraftPackage / overwriteDraftPackage). When directory is omitted, writes under workshop/<pipeline-id> on registered catalogs (or <pipeline-id> on the workspace catalog). The host adds registered workshop paths to stageflow.yaml so Run can see them. Pass directory only when the operator names a folder. Soft undo does not reverse disk. Prefer mode auto; set allowInvalid only when the operator explicitly requests saving invalid YAML. Do not pass projectRoot — the host binds it.",
812
+ parameters: Type.Object({
813
+ directory: Type.Optional(Type.String()),
814
+ pipelineFilename: Type.Optional(Type.String()),
815
+ mode: Type.Optional(Type.Union([Type.Literal("auto"), Type.Literal("create"), Type.Literal("overwrite")])),
816
+ allowInvalid: Type.Optional(Type.Boolean()),
817
+ }, closed),
630
818
  async handler(args, ctx) {
631
819
  const writeTarget = await resolveSaveWriteTarget(ctx, options);
632
820
  if (!writeTarget.ok) {
@@ -1,3 +1,4 @@
1
+ import type { TSchema } from "typebox";
1
2
  import type { DraftPackage } from "../config/draftPackage.js";
2
3
  import type { ValidationResult } from "../config/validateCatalog.js";
3
4
  import type { AcceptProposalResult, ProposalArtifactDiff, UndoMutationResult } from "./proposals.js";
@@ -10,6 +11,7 @@ export type OperatorAgentToolHandler = (args: Record<string, unknown>, ctx: Oper
10
11
  export type OperatorAgentTool = {
11
12
  name: string;
12
13
  description: string;
14
+ parameters: TSchema;
13
15
  handler: OperatorAgentToolHandler;
14
16
  };
15
17
  export type OperatorAgentToolContext = {
@@ -17,6 +19,14 @@ export type OperatorAgentToolContext = {
17
19
  setContext(next: unknown): void;
18
20
  /** Apply mutation immediately and record an undo receipt. */
19
21
  emitProposal(proposal: OperatorAgentProposal): void;
22
+ /**
23
+ * Set only on an eval session. Returning a result skips the tool handler.
24
+ * Returning null lets the handler run. Absent on Workshop, author and Guide sessions.
25
+ */
26
+ evalGate?: {
27
+ decide(name: string, args: Record<string, unknown>): OperatorAgentToolResult | null;
28
+ observe(name: string, result: OperatorAgentToolResult): void;
29
+ };
20
30
  };
21
31
  export type OperatorAgentProposal = {
22
32
  /** Mutation id (Accept confirms; Reject / undoMutation soft-undos). */
@@ -72,6 +82,10 @@ export type WorkshopToolActivityUpdate = {
72
82
  draft?: DraftPackage;
73
83
  /** Build the turn is editing. Absent when the chat is unlinked. */
74
84
  buildId?: string;
85
+ /** Tool arguments, on `running`. */
86
+ args?: Record<string, unknown>;
87
+ /** Tool result `content`, on `complete`. */
88
+ result?: unknown;
75
89
  };
76
90
  export type OperatorAgentSession = {
77
91
  readonly profileId: string;
@@ -111,6 +125,15 @@ export type OperatorAgentHost = {
111
125
  profileId: string;
112
126
  context: unknown;
113
127
  }): OperatorAgentSession;
128
+ /**
129
+ * A session whose tools pass through `gate` before they run. The gate is required:
130
+ * an eval session with no gate is refused.
131
+ */
132
+ openEvalSession(input: {
133
+ profileId: string;
134
+ context: unknown;
135
+ gate: NonNullable<OperatorAgentToolContext["evalGate"]>;
136
+ }): OperatorAgentSession;
114
137
  };
115
138
  export type WorkshopDraftContext = {
116
139
  draft: DraftPackage;
@@ -1,3 +1,3 @@
1
1
  export declare const PACKAGE_NAME = "stageflow";
2
- export declare const PACKAGE_VERSION = "0.42.0";
2
+ export declare const PACKAGE_VERSION = "0.44.0";
3
3
  export declare const BUILD_SHA: string;
@@ -1,3 +1,3 @@
1
1
  export const PACKAGE_NAME = "stageflow";
2
- export const PACKAGE_VERSION = "0.42.0";
2
+ export const PACKAGE_VERSION = "0.44.0";
3
3
  export const BUILD_SHA = process.env.STAGEFLOW_BUILD_SHA ?? "unknown";
@@ -1,4 +1,5 @@
1
1
  import type { RunMeta, RunStatus } from "./port.js";
2
+ import { type RunClockTerminalStatus } from "./runStatusClock.js";
2
3
  export type RetentionDecision = "none" | "slim" | "purge";
3
4
  export type StatusRetentionWindow = {
4
5
  slimMs: number;
@@ -24,4 +25,4 @@ export declare function slimArtifactMaxBytesFromEnv(env?: NodeJS.ProcessEnv): nu
24
25
  * Already-slimmed rows past SLIM but not PURGE return `"none"` (R18).
25
26
  */
26
27
  export declare function retentionDecision(row: RetentionRow, now: Date, windows?: RetentionWindows): RetentionDecision;
27
- export declare function isTerminalRunStatus(status: RunStatus | undefined): status is "succeeded" | "failed" | "cancelled";
28
+ export declare function isTerminalRunStatus(status: RunStatus | undefined): status is RunClockTerminalStatus;
@@ -1,3 +1,4 @@
1
+ import { isRunClockTerminal, } from "./runStatusClock.js";
1
2
  export const DAY_MS = 24 * 60 * 60 * 1000;
2
3
  /** Defaults: succeeded 3d/30d; failed and cancelled 30d/90d (R30 / AE6). */
3
4
  export const DEFAULT_RETENTION_WINDOWS = {
@@ -7,11 +8,6 @@ export const DEFAULT_RETENTION_WINDOWS = {
7
8
  };
8
9
  export const DEFAULT_BARE_CACHE_TTL_MS = 30 * DAY_MS;
9
10
  export const DEFAULT_SLIM_ARTIFACT_MAX_BYTES = 1024 * 1024;
10
- const TERMINAL = new Set([
11
- "succeeded",
12
- "failed",
13
- "cancelled",
14
- ]);
15
11
  function parsePositiveMs(raw) {
16
12
  if (raw === undefined)
17
13
  return undefined;
@@ -52,7 +48,7 @@ export function slimArtifactMaxBytesFromEnv(env = process.env) {
52
48
  */
53
49
  export function retentionDecision(row, now, windows = DEFAULT_RETENTION_WINDOWS) {
54
50
  const status = row.status;
55
- if (status === undefined || !TERMINAL.has(status))
51
+ if (!isRunClockTerminal(status))
56
52
  return "none";
57
53
  if (row.finished_at === undefined || row.finished_at === "")
58
54
  return "none";
@@ -70,5 +66,5 @@ export function retentionDecision(row, now, windows = DEFAULT_RETENTION_WINDOWS)
70
66
  return "none";
71
67
  }
72
68
  export function isTerminalRunStatus(status) {
73
- return status !== undefined && TERMINAL.has(status);
69
+ return isRunClockTerminal(status);
74
70
  }
@@ -0,0 +1,17 @@
1
+ export declare const RUN_CLOCK_TERMINAL_STATUSES: readonly ["succeeded", "failed", "cancelled"];
2
+ export type RunClockTerminalStatus = (typeof RUN_CLOCK_TERMINAL_STATUSES)[number];
3
+ export type RunClockStatus = "created" | "queued" | "running" | RunClockTerminalStatus;
4
+ export declare function isRunClockTerminal(status: string | undefined): status is RunClockTerminalStatus;
5
+ export type FinishedAtDecision = {
6
+ action: "stamp";
7
+ finishedAt: string;
8
+ } | {
9
+ action: "keep";
10
+ } | {
11
+ action: "clear";
12
+ };
13
+ export declare function nextFinishedAt(input: {
14
+ nextStatus: RunClockStatus;
15
+ currentFinishedAt: string | null | undefined;
16
+ now: string;
17
+ }): FinishedAtDecision;