stageflow 0.2.0 → 0.8.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 (224) hide show
  1. package/README.md +179 -14
  2. package/dist/agent/activity.d.ts +12 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/cursorProvider.d.ts +1 -10
  7. package/dist/agent/cursorProvider.js +18 -10
  8. package/dist/agent/fakeAgent.js +15 -7
  9. package/dist/agent/piAdapter.d.ts +18 -1
  10. package/dist/agent/piAdapter.js +115 -45
  11. package/dist/agent/port.d.ts +7 -0
  12. package/dist/agent/port.js +3 -0
  13. package/dist/agent/providerAuth.js +12 -7
  14. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  15. package/dist/catalog/displayCatalogPath.js +28 -0
  16. package/dist/cli/artifactCommand.d.ts +10 -0
  17. package/dist/cli/artifactCommand.js +120 -0
  18. package/dist/cli/envelopeCommand.d.ts +32 -0
  19. package/dist/cli/envelopeCommand.js +285 -0
  20. package/dist/cli/exportRunCommand.d.ts +10 -0
  21. package/dist/cli/exportRunCommand.js +150 -0
  22. package/dist/cli/handoffFormat.d.ts +22 -0
  23. package/dist/cli/handoffFormat.js +61 -0
  24. package/dist/cli/initCommand.d.ts +9 -0
  25. package/dist/cli/initCommand.js +71 -0
  26. package/dist/cli/initTemplates.d.ts +3 -0
  27. package/dist/cli/initTemplates.js +19 -0
  28. package/dist/cli/operatorCatalog.d.ts +10 -0
  29. package/dist/cli/operatorCatalog.js +16 -0
  30. package/dist/cli/runCommand.d.ts +7 -1
  31. package/dist/cli/runCommand.js +83 -11
  32. package/dist/cli/runOutput.d.ts +4 -1
  33. package/dist/cli/runOutput.js +36 -8
  34. package/dist/cli/skillsCommand.d.ts +16 -0
  35. package/dist/cli/skillsCommand.js +459 -0
  36. package/dist/cli/validateCommand.d.ts +1 -1
  37. package/dist/cli/validateCommand.js +20 -3
  38. package/dist/cli.d.ts +1 -0
  39. package/dist/cli.js +128 -22
  40. package/dist/config/browseCatalog.d.ts +45 -0
  41. package/dist/config/browseCatalog.js +170 -0
  42. package/dist/config/createPipeline.d.ts +12 -3
  43. package/dist/config/createPipeline.js +292 -100
  44. package/dist/config/createStage.d.ts +10 -4
  45. package/dist/config/createStage.js +44 -14
  46. package/dist/config/listConfig.d.ts +14 -19
  47. package/dist/config/listConfig.js +26 -191
  48. package/dist/config/listModelsFromManifest.d.ts +2 -0
  49. package/dist/config/listModelsFromManifest.js +4 -0
  50. package/dist/config/loadOutcome.d.ts +4 -0
  51. package/dist/config/loadPipeline.d.ts +1 -15
  52. package/dist/config/loadPipeline.js +88 -142
  53. package/dist/config/loadStage.d.ts +4 -0
  54. package/dist/config/loadStage.js +60 -32
  55. package/dist/config/loadStageflowManifest.d.ts +5 -0
  56. package/dist/config/loadStageflowManifest.js +157 -0
  57. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  58. package/dist/config/mergePipelineIncludes.js +141 -0
  59. package/dist/config/normalizePipelineStageEntry.d.ts +18 -0
  60. package/dist/config/normalizePipelineStageEntry.js +210 -0
  61. package/dist/config/pipelineStageKeys.d.ts +4 -0
  62. package/dist/config/pipelineStageKeys.js +14 -0
  63. package/dist/config/resolveCatalogContext.d.ts +13 -0
  64. package/dist/config/resolveCatalogContext.js +13 -0
  65. package/dist/config/resolveForkEmitContext.d.ts +4 -0
  66. package/dist/config/resolveForkEmitContext.js +34 -0
  67. package/dist/config/resolvePipelineDag.d.ts +5 -1
  68. package/dist/config/resolvePipelineDag.js +86 -36
  69. package/dist/config/scanCatalogPaths.d.ts +4 -0
  70. package/dist/config/scanCatalogPaths.js +83 -0
  71. package/dist/config/validateCatalog.d.ts +22 -4
  72. package/dist/config/validateCatalog.js +169 -132
  73. package/dist/envelope/check.d.ts +2 -0
  74. package/dist/envelope/check.js +58 -0
  75. package/dist/envelope/cloneForks.d.ts +2 -0
  76. package/dist/envelope/cloneForks.js +62 -0
  77. package/dist/envelope/forkChoice.d.ts +3 -0
  78. package/dist/envelope/forkChoice.js +47 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.js +6 -0
  81. package/dist/mcp/catalogTools.d.ts +3 -0
  82. package/dist/mcp/catalogTools.js +187 -0
  83. package/dist/mcp/controlTools.d.ts +3 -0
  84. package/dist/mcp/controlTools.js +147 -0
  85. package/dist/mcp/deps.d.ts +11 -0
  86. package/dist/mcp/deps.js +1 -0
  87. package/dist/mcp/projectRun.d.ts +1 -37
  88. package/dist/mcp/projectRun.js +1 -57
  89. package/dist/mcp/resources.d.ts +5 -0
  90. package/dist/mcp/resources.js +45 -0
  91. package/dist/mcp/server.d.ts +16 -3
  92. package/dist/mcp/server.js +159 -3
  93. package/dist/mcp/toolResults.d.ts +7 -0
  94. package/dist/mcp/toolResults.js +6 -0
  95. package/dist/mcp/tools.d.ts +2 -7
  96. package/dist/mcp/tools.js +52 -86
  97. package/dist/mcp/waitRun.d.ts +46 -0
  98. package/dist/mcp/waitRun.js +146 -0
  99. package/dist/mcp/waitingGates.d.ts +12 -0
  100. package/dist/mcp/waitingGates.js +83 -0
  101. package/dist/project/findProjectRoot.d.ts +2 -0
  102. package/dist/project/findProjectRoot.js +51 -0
  103. package/dist/project/globalHome.d.ts +2 -0
  104. package/dist/project/globalHome.js +19 -0
  105. package/dist/project/resolveProjectContext.d.ts +9 -0
  106. package/dist/project/resolveProjectContext.js +38 -0
  107. package/dist/project/resolveStageflowContext.d.ts +11 -0
  108. package/dist/project/resolveStageflowContext.js +66 -0
  109. package/dist/projection/projectRun.d.ts +44 -0
  110. package/dist/projection/projectRun.js +71 -0
  111. package/dist/prompt/priorEnvelope.d.ts +1 -1
  112. package/dist/prompt/priorEnvelope.js +4 -1
  113. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  114. package/dist/runstore/normalizeCatalogPath.js +4 -0
  115. package/dist/runstore/paths.js +2 -0
  116. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  117. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  118. package/dist/runstore/port.d.ts +22 -2
  119. package/dist/runstore/port.js +4 -1
  120. package/dist/runstore/runProjection.d.ts +1 -1
  121. package/dist/runstore/runProjection.js +25 -4
  122. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  123. package/dist/runstore/sqlite/SqliteRunStore.js +65 -7
  124. package/dist/runstore/sqlite/schema.d.ts +1 -1
  125. package/dist/runstore/sqlite/schema.js +4 -1
  126. package/dist/runstore/stageInstanceId.d.ts +4 -0
  127. package/dist/runstore/stageInstanceId.js +23 -0
  128. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  129. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  130. package/dist/runstore/trackProjection.js +28 -12
  131. package/dist/runstore/workspaceLayout.d.ts +1 -0
  132. package/dist/runstore/workspaceLayout.js +1 -1
  133. package/dist/runtime/cloneSchedule.d.ts +18 -0
  134. package/dist/runtime/cloneSchedule.js +233 -0
  135. package/dist/runtime/credentialBinding.d.ts +8 -6
  136. package/dist/runtime/credentialBinding.js +18 -24
  137. package/dist/runtime/envelopeRouting.d.ts +2 -1
  138. package/dist/runtime/envelopeRouting.js +80 -13
  139. package/dist/runtime/pipelineRunner.d.ts +3 -0
  140. package/dist/runtime/pipelineRunner.js +15 -1
  141. package/dist/runtime/pipelineScheduler.d.ts +8 -2
  142. package/dist/runtime/pipelineScheduler.js +215 -39
  143. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  144. package/dist/runtime/reloadRunCatalog.js +44 -0
  145. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  146. package/dist/runtime/resumeReconstruct.js +31 -10
  147. package/dist/runtime/runChangeBus.d.ts +18 -0
  148. package/dist/runtime/runChangeBus.js +83 -0
  149. package/dist/runtime/runManager.d.ts +4 -0
  150. package/dist/runtime/runManager.js +36 -16
  151. package/dist/runtime/settingsFile.d.ts +12 -0
  152. package/dist/runtime/settingsFile.js +87 -6
  153. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  154. package/dist/runtime/stageAttemptBootstrap.js +18 -7
  155. package/dist/runtime/stageProcessLauncher.js +3 -0
  156. package/dist/runtime/stageRecovery.js +3 -1
  157. package/dist/runtime/stageRunner.d.ts +1 -0
  158. package/dist/runtime/stageRunner.js +17 -15
  159. package/dist/runtime/stageWorker.js +13 -10
  160. package/dist/server/bootstrap.d.ts +32 -0
  161. package/dist/server/bootstrap.js +56 -0
  162. package/dist/server/createHttpHost.d.ts +34 -0
  163. package/dist/server/createHttpHost.js +70 -0
  164. package/dist/server/http.d.ts +6 -13
  165. package/dist/server/http.js +297 -352
  166. package/dist/server/mcpHost.d.ts +22 -0
  167. package/dist/server/mcpHost.js +21 -0
  168. package/dist/server/operatorResults.d.ts +13 -0
  169. package/dist/server/operatorResults.js +22 -0
  170. package/dist/tools/emitStageEnvelope.d.ts +12 -1
  171. package/dist/tools/emitStageEnvelope.js +27 -1
  172. package/dist/types/envelope.d.ts +3 -0
  173. package/dist/types/forkChoice.d.ts +30 -0
  174. package/dist/types/forkChoice.js +1 -0
  175. package/dist/types/pipeline.d.ts +59 -2
  176. package/dist/types/stageflowManifest.d.ts +23 -0
  177. package/dist/types/stageflowManifest.js +1 -0
  178. package/dist/ui/assets/{index-DCsDopak.css → index-C3N7MXAC.css} +1 -1
  179. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  180. package/dist/ui/index.html +3 -2
  181. package/dist/ui/stageflow-icon.svg +12 -0
  182. package/package.json +8 -5
  183. package/skills/install-suite.sh +107 -0
  184. package/skills/stageflow/SKILL.md +26 -0
  185. package/skills/stageflow/references/control-surface.md +26 -0
  186. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  187. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  188. package/skills/stageflow-author/SKILL.md +58 -0
  189. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  190. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  191. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  192. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  193. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  194. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  195. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  196. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  197. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  198. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  199. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  200. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  201. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  202. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  203. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  204. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  205. package/skills/stageflow-delegate/SKILL.md +18 -0
  206. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  207. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  208. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  209. package/skills/stageflow-run/SKILL.md +151 -0
  210. package/skills/stageflow-run/references/mcp-call.md +17 -0
  211. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  212. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  213. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  214. package/skills/stageflow-session-capture/SKILL.md +94 -0
  215. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  216. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  217. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  218. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  219. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  220. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  221. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  222. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  223. package/skills/stageflow-setup/SKILL.md +110 -0
  224. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -1,16 +1,67 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
2
- import { McpServer } from "@modelcontextprotocol/server";
3
+ import { isInitializeRequest, McpServer, } from "@modelcontextprotocol/server";
4
+ import { registerRunResources, runResourceUri } from "./resources.js";
3
5
  import { registerMcpTools } from "./tools.js";
6
+ function jsonRpcError(res, status, code, message) {
7
+ const payload = JSON.stringify({
8
+ jsonrpc: "2.0",
9
+ error: { code, message },
10
+ id: null,
11
+ });
12
+ res.writeHead(status, {
13
+ "Content-Type": "application/json; charset=utf-8",
14
+ "Content-Length": Buffer.byteLength(payload),
15
+ });
16
+ res.end(payload);
17
+ }
18
+ async function readJsonBody(req) {
19
+ const chunks = [];
20
+ for await (const chunk of req) {
21
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
22
+ }
23
+ if (chunks.length === 0)
24
+ return undefined;
25
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
26
+ }
27
+ function sessionIdFromRequest(req) {
28
+ const raw = req.headers["mcp-session-id"];
29
+ if (typeof raw === "string" && raw.length > 0)
30
+ return raw;
31
+ if (Array.isArray(raw) && typeof raw[0] === "string" && raw[0].length > 0) {
32
+ return raw[0];
33
+ }
34
+ return undefined;
35
+ }
36
+ function toolDepsFrom(deps) {
37
+ return { manager: deps.manager, store: deps.store, cwd: deps.cwd };
38
+ }
39
+ function attachBusNotifications(server, bus) {
40
+ return bus.on((event) => {
41
+ const uri = runResourceUri(event.runId);
42
+ void server.server.sendResourceUpdated({ uri }).catch(() => undefined);
43
+ if (event.kind === "created") {
44
+ void server.server.sendResourceListChanged().catch(() => undefined);
45
+ }
46
+ });
47
+ }
4
48
  export function createStageflowMcpServer(deps) {
5
49
  const server = new McpServer({
6
50
  name: "stageflow",
7
51
  version: "0.2.0",
52
+ }, {
53
+ capabilities: {
54
+ resources: {
55
+ subscribe: true,
56
+ listChanged: true,
57
+ },
58
+ },
8
59
  });
9
60
  registerMcpTools(server, deps);
61
+ registerRunResources(server, deps);
10
62
  return server;
11
63
  }
12
- /** Stateless Streamable HTTP: new server + transport per request. */
13
- export async function handleMcpHttpRequest(req, res, deps) {
64
+ async function handleStatelessMcpRequest(req, res, deps) {
14
65
  const server = createStageflowMcpServer(deps);
15
66
  const transport = new NodeStreamableHTTPServerTransport({
16
67
  sessionIdGenerator: undefined,
@@ -24,3 +75,108 @@ export async function handleMcpHttpRequest(req, res, deps) {
24
75
  await server.close().catch(() => undefined);
25
76
  }
26
77
  }
78
+ /** Stateful Streamable HTTP session host (product default) or stateless escape hatch. */
79
+ export function createMcpHttpHandler(deps, options = {}) {
80
+ const stateless = options.mcpStateless === true;
81
+ const sessions = new Map();
82
+ const tools = toolDepsFrom(deps);
83
+ async function disposeSession(sessionId, opts) {
84
+ const entry = sessions.get(sessionId);
85
+ if (!entry)
86
+ return;
87
+ sessions.delete(sessionId);
88
+ const unsub = entry.unsubscribeBus;
89
+ entry.unsubscribeBus = undefined;
90
+ unsub?.();
91
+ if (opts.closeTransport) {
92
+ await entry.transport.close().catch(() => undefined);
93
+ }
94
+ await entry.server.close().catch(() => undefined);
95
+ }
96
+ async function closeSession(sessionId) {
97
+ await disposeSession(sessionId, { closeTransport: true });
98
+ }
99
+ async function handleStateful(req, res) {
100
+ const sessionId = sessionIdFromRequest(req);
101
+ const method = req.method ?? "GET";
102
+ if (sessionId && sessions.has(sessionId)) {
103
+ const entry = sessions.get(sessionId);
104
+ await entry.transport.handleRequest(req, res);
105
+ return;
106
+ }
107
+ if (sessionId) {
108
+ jsonRpcError(res, 404, -32001, "Session not found");
109
+ return;
110
+ }
111
+ if (method === "POST") {
112
+ let body;
113
+ try {
114
+ body = await readJsonBody(req);
115
+ }
116
+ catch {
117
+ jsonRpcError(res, 400, -32700, "Parse error");
118
+ return;
119
+ }
120
+ if (!isInitializeRequest(body)) {
121
+ jsonRpcError(res, 400, -32000, "Bad Request: Session ID required");
122
+ return;
123
+ }
124
+ const server = createStageflowMcpServer(tools);
125
+ let sessionRegistered = false;
126
+ const transport = new NodeStreamableHTTPServerTransport({
127
+ sessionIdGenerator: () => randomUUID(),
128
+ onsessioninitialized: (id) => {
129
+ sessions.set(id, {
130
+ transport,
131
+ server,
132
+ unsubscribeBus: attachBusNotifications(server, deps.runChangeBus),
133
+ });
134
+ sessionRegistered = true;
135
+ },
136
+ onsessionclosed: (id) => {
137
+ void disposeSession(id, { closeTransport: false });
138
+ },
139
+ });
140
+ transport.onclose = () => {
141
+ const id = transport.sessionId;
142
+ if (!id)
143
+ return;
144
+ void disposeSession(id, { closeTransport: false });
145
+ };
146
+ try {
147
+ await server.connect(transport);
148
+ await transport.handleRequest(req, res, body);
149
+ }
150
+ finally {
151
+ if (!sessionRegistered) {
152
+ await transport.close().catch(() => undefined);
153
+ await server.close().catch(() => undefined);
154
+ }
155
+ }
156
+ return;
157
+ }
158
+ jsonRpcError(res, 400, -32000, "Bad Request: Session ID required");
159
+ }
160
+ return {
161
+ handle: async (req, res) => {
162
+ if (stateless) {
163
+ await handleStatelessMcpRequest(req, res, tools);
164
+ return;
165
+ }
166
+ await handleStateful(req, res);
167
+ },
168
+ close: async () => {
169
+ const ids = [...sessions.keys()];
170
+ await Promise.all(ids.map((id) => closeSession(id)));
171
+ },
172
+ };
173
+ }
174
+ export async function handleMcpHttpRequest(req, res, deps) {
175
+ await handleStatelessMcpRequest(req, res, deps);
176
+ }
177
+ export function resolveMcpStateless(options) {
178
+ if (options?.mcpStateless === true)
179
+ return true;
180
+ const env = options?.env ?? process.env;
181
+ return env.STAGEFLOW_MCP_STATELESS === "1";
182
+ }
@@ -0,0 +1,7 @@
1
+ export declare function textResult(data: unknown, isError?: boolean): {
2
+ content: {
3
+ type: "text";
4
+ text: string;
5
+ }[];
6
+ isError?: boolean | undefined;
7
+ };
@@ -0,0 +1,6 @@
1
+ export function textResult(data, isError = false) {
2
+ return {
3
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
4
+ ...(isError ? { isError: true } : {}),
5
+ };
6
+ }
@@ -1,9 +1,4 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/server";
2
- import type { RunStore } from "../runstore/port.js";
3
- import type { RunManager } from "../runtime/runManager.js";
4
- export type McpToolDeps = {
5
- manager: RunManager;
6
- store: RunStore;
7
- cwd: string;
8
- };
2
+ import type { McpToolDeps } from "./deps.js";
3
+ export type { McpToolDeps };
9
4
  export declare function registerMcpTools(server: McpServer, deps: McpToolDeps): void;
package/dist/mcp/tools.js CHANGED
@@ -1,96 +1,62 @@
1
1
  import { z } from "zod";
2
- import { listPipelines } from "../config/listConfig.js";
3
- import { projectRunForMcp } from "./projectRun.js";
4
- import { readRunArtifact } from "./readArtifact.js";
5
- const taskFileSchema = z.object({
6
- id: z.string(),
7
- goal: z.string(),
8
- context: z.string().optional(),
9
- constraints: z.string().optional(),
10
- checkout: z.string().optional(),
11
- });
12
- function textResult(data, isError = false) {
13
- return {
14
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
15
- ...(isError ? { isError: true } : {}),
16
- };
17
- }
2
+ import { registerCatalogTools } from "./catalogTools.js";
3
+ import { registerControlTools } from "./controlTools.js";
4
+ import { textResult } from "./toolResults.js";
5
+ import { DEFAULT_TIMEOUT_MS, waitRun } from "./waitRun.js";
18
6
  export function registerMcpTools(server, deps) {
19
- const { manager, store, cwd } = deps;
20
- server.registerTool("list_pipelines", {
21
- description: "List runnable pipeline ids from the factory cwd",
22
- inputSchema: z.object({}),
23
- }, async () => {
24
- const pipelines = await listPipelines(cwd);
25
- return textResult({ pipelines });
26
- });
27
- server.registerTool("list_runs", {
28
- description: "List known pipeline runs",
29
- inputSchema: z.object({}),
30
- }, async () => {
31
- const runs = await store.listRuns();
32
- return textResult({ runs });
33
- });
34
- server.registerTool("get_health", {
35
- description: "Server health and soft-max run capacity: activeRunIds, activeCount, maxConcurrent, slotsAvailable. Start until slotsAvailable is 0; then wait for a run to finish or raise STAGEFLOW_MAX_CONCURRENT_RUNS.",
36
- inputSchema: z.object({}),
37
- }, async () => textResult(manager.getHealth()));
38
- server.registerTool("start_run", {
39
- description: "Start a pipeline with an inline TaskFile (not written under tasks/). Returns { runId }. On conflict returns isError with code busy_capacity (soft max full) or busy_checkout (same checkout leased), plus activeCount/maxConcurrent/activeRunIds and optional conflictingRunId/conflictingCheckout.",
40
- inputSchema: z.object({
41
- pipeline: z.string(),
42
- task: taskFileSchema,
43
- }),
44
- }, async ({ pipeline, task }) => {
45
- if (!pipeline.trim()) {
46
- return textResult({ error: "pipeline is required" }, true);
47
- }
48
- const result = await manager.startRun({ pipeline, task });
49
- if (!result.ok) {
50
- const { ok: _ok, reason, ...rest } = result;
51
- return textResult({ error: reason, ...rest }, true);
52
- }
53
- return textResult({ runId: result.runId });
54
- });
55
- server.registerTool("get_run", {
56
- description: "Poll a run: status, stage statuses, and envelope summary/payload/artifact paths (no events).",
7
+ const { store } = deps;
8
+ registerCatalogTools(server, deps);
9
+ registerControlTools(server, deps);
10
+ server.registerTool("wait_run", {
11
+ description: "Long-poll until a run is waiting for HITL input and/or terminal (succeeded/failed), or until timeout_ms. Default until=any wakes on waiting OR terminal. Abort cancels only this wait (not the run). Optional notifications/progress when the client supplies _meta.progressToken. Compose: wait_run → answer_gate → wait_run.",
57
12
  inputSchema: z.object({
58
13
  runId: z.string(),
14
+ timeout_ms: z
15
+ .number()
16
+ .optional()
17
+ .describe(`Wait budget in ms (default ${DEFAULT_TIMEOUT_MS}, max 240000)`),
18
+ until: z
19
+ .enum(["any", "waiting", "terminal"])
20
+ .optional()
21
+ .describe('Wake predicate: "any" (default), "waiting", or "terminal"'),
59
22
  }),
60
- }, async ({ runId }) => {
61
- try {
62
- const detail = await store.readRun(runId);
63
- return textResult(projectRunForMcp(detail));
64
- }
65
- catch (err) {
66
- const message = err instanceof Error ? err.message : String(err);
67
- const notFound = /not found|no such|unknown run/i.test(message);
68
- return textResult({
69
- error: message,
70
- status: notFound ? 404 : 500,
71
- }, true);
72
- }
73
- });
74
- server.registerTool("read_artifact", {
75
- description: "Read a text artifact from a run workspace by relative path (contained under the run workspace).",
76
- inputSchema: z.object({
77
- runId: z.string(),
78
- path: z.string(),
79
- }),
80
- }, async ({ runId, path: artifactPath }) => {
81
- try {
82
- const content = await readRunArtifact(store, runId, artifactPath);
83
- return textResult({ runId, path: artifactPath, content });
84
- }
85
- catch (err) {
86
- const message = err instanceof Error ? err.message : String(err);
87
- const notFound = message.startsWith("Run not found") ||
88
- message.startsWith("Artifact not found") ||
89
- /no such|not found/i.test(message);
23
+ }, async ({ runId, timeout_ms, until }, ctx) => {
24
+ const progressToken = ctx.mcpReq._meta?.progressToken;
25
+ const result = await waitRun({
26
+ store,
27
+ runId,
28
+ timeoutMs: timeout_ms,
29
+ until,
30
+ signal: ctx.mcpReq.signal,
31
+ onProgress: progressToken !== undefined
32
+ ? async (info) => {
33
+ try {
34
+ await ctx.mcpReq.notify({
35
+ method: "notifications/progress",
36
+ params: {
37
+ progressToken,
38
+ progress: info.progress,
39
+ message: info.message,
40
+ },
41
+ });
42
+ }
43
+ catch {
44
+ }
45
+ }
46
+ : undefined,
47
+ });
48
+ if (!result.ok) {
90
49
  return textResult({
91
- error: message,
92
- status: notFound ? 404 : 400,
50
+ error: result.error,
51
+ ...(result.status !== undefined ? { status: result.status } : {}),
52
+ ...(result.code !== undefined ? { code: result.code } : {}),
93
53
  }, true);
94
54
  }
55
+ return textResult({
56
+ reason: result.reason,
57
+ elapsed_ms: result.elapsed_ms,
58
+ until: result.until,
59
+ run: result.run,
60
+ });
95
61
  });
96
62
  }
@@ -0,0 +1,46 @@
1
+ import type { RunProjection } from "../projection/projectRun.js";
2
+ import type { RunStore } from "../runstore/port.js";
3
+ export type WaitUntil = "any" | "waiting" | "terminal";
4
+ export type WaitReason = "waiting" | "terminal" | "timeout" | "already";
5
+ export declare const DEFAULT_TIMEOUT_MS = 60000;
6
+ export declare const MAX_TIMEOUT_MS = 240000;
7
+ export declare const POLL_INTERVAL_MS = 250;
8
+ export declare const PROGRESS_EVERY_MS = 2000;
9
+ export type WaitRunProgressInfo = {
10
+ progress: number;
11
+ message: string;
12
+ };
13
+ export type WaitRunSuccess = {
14
+ ok: true;
15
+ reason: WaitReason;
16
+ elapsed_ms: number;
17
+ until: WaitUntil;
18
+ run: RunProjection;
19
+ };
20
+ export type WaitRunFailure = {
21
+ ok: false;
22
+ error: string;
23
+ status?: number;
24
+ code?: string;
25
+ };
26
+ export type WaitRunResult = WaitRunSuccess | WaitRunFailure;
27
+ export type WaitRunOpts = {
28
+ store: Pick<RunStore, "readRun">;
29
+ runId: string;
30
+ timeoutMs?: number;
31
+ until?: WaitUntil;
32
+ signal?: AbortSignal;
33
+ onProgress?: (info: WaitRunProgressInfo) => void | Promise<void>;
34
+ };
35
+ export declare function clampTimeoutMs(timeoutMs: number | undefined): {
36
+ ok: true;
37
+ value: number;
38
+ } | {
39
+ ok: false;
40
+ error: string;
41
+ };
42
+ export declare function isWaitingProjection(projection: Pick<RunProjection, "waiting_stage_ids" | "waiting_stage_id" | "stages">): boolean;
43
+ export declare function isTerminalProjection(projection: Pick<RunProjection, "status">): boolean;
44
+ export declare function classifyWaitWake(projection: Pick<RunProjection, "status" | "waiting_stage_ids" | "waiting_stage_id" | "stages">, until: WaitUntil, hadWaited: boolean): Exclude<WaitReason, "timeout"> | null;
45
+ export declare function sleepAbortable(ms: number, signal?: AbortSignal): Promise<void>;
46
+ export declare function waitRun(opts: WaitRunOpts): Promise<WaitRunResult>;
@@ -0,0 +1,146 @@
1
+ import { projectRunForMcp } from "./projectRun.js";
2
+ export const DEFAULT_TIMEOUT_MS = 60_000;
3
+ export const MAX_TIMEOUT_MS = 240_000;
4
+ export const POLL_INTERVAL_MS = 250;
5
+ export const PROGRESS_EVERY_MS = 2_000;
6
+ export function clampTimeoutMs(timeoutMs) {
7
+ const value = timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : timeoutMs;
8
+ if (typeof value !== "number" ||
9
+ !Number.isFinite(value) ||
10
+ value <= 0 ||
11
+ value > MAX_TIMEOUT_MS) {
12
+ return {
13
+ ok: false,
14
+ error: `timeout_ms must be in (0, ${MAX_TIMEOUT_MS}]`,
15
+ };
16
+ }
17
+ return { ok: true, value };
18
+ }
19
+ export function isWaitingProjection(projection) {
20
+ if (projection.waiting_stage_ids !== undefined &&
21
+ projection.waiting_stage_ids.length > 0) {
22
+ return true;
23
+ }
24
+ if (projection.waiting_stage_id !== undefined) {
25
+ return true;
26
+ }
27
+ return projection.stages.some((s) => s.status === "waiting_for_input");
28
+ }
29
+ export function isTerminalProjection(projection) {
30
+ return projection.status === "succeeded" || projection.status === "failed";
31
+ }
32
+ export function classifyWaitWake(projection, until, hadWaited) {
33
+ const waiting = isWaitingProjection(projection);
34
+ const terminal = isTerminalProjection(projection);
35
+ let matched = null;
36
+ if (until === "waiting") {
37
+ if (waiting)
38
+ matched = "waiting";
39
+ else if (terminal)
40
+ matched = "terminal";
41
+ }
42
+ else if (until === "terminal") {
43
+ if (terminal)
44
+ matched = "terminal";
45
+ }
46
+ else if (waiting) {
47
+ matched = "waiting";
48
+ }
49
+ else if (terminal) {
50
+ matched = "terminal";
51
+ }
52
+ if (matched === null)
53
+ return null;
54
+ return hadWaited ? matched : "already";
55
+ }
56
+ export function sleepAbortable(ms, signal) {
57
+ return new Promise((resolve, reject) => {
58
+ if (signal?.aborted) {
59
+ reject(Object.assign(new Error("aborted"), { code: "aborted" }));
60
+ return;
61
+ }
62
+ const timer = setTimeout(() => {
63
+ signal?.removeEventListener("abort", onAbort);
64
+ resolve();
65
+ }, ms);
66
+ const onAbort = () => {
67
+ clearTimeout(timer);
68
+ reject(Object.assign(new Error("aborted"), { code: "aborted" }));
69
+ };
70
+ signal?.addEventListener("abort", onAbort, { once: true });
71
+ });
72
+ }
73
+ export async function waitRun(opts) {
74
+ const untilVal = opts.until ?? "any";
75
+ const clamped = clampTimeoutMs(opts.timeoutMs);
76
+ if (!clamped.ok) {
77
+ return { ok: false, error: clamped.error, status: 400 };
78
+ }
79
+ const signal = opts.signal;
80
+ const started = Date.now();
81
+ let hadWaited = false;
82
+ let pollCount = 0;
83
+ let lastProgressAt = 0;
84
+ while (true) {
85
+ if (signal?.aborted) {
86
+ return { ok: false, error: "wait aborted", code: "aborted" };
87
+ }
88
+ let detail;
89
+ try {
90
+ detail = await opts.store.readRun(opts.runId);
91
+ }
92
+ catch (err) {
93
+ const message = err instanceof Error ? err.message : String(err);
94
+ const notFound = /not found|no such|unknown run/i.test(message);
95
+ return {
96
+ ok: false,
97
+ error: message,
98
+ status: notFound ? 404 : 500,
99
+ };
100
+ }
101
+ const run = projectRunForMcp(detail);
102
+ const wake = classifyWaitWake(run, untilVal, hadWaited);
103
+ const elapsed_ms = Date.now() - started;
104
+ if (wake !== null) {
105
+ return {
106
+ ok: true,
107
+ reason: wake,
108
+ elapsed_ms,
109
+ until: untilVal,
110
+ run,
111
+ };
112
+ }
113
+ if (elapsed_ms >= clamped.value) {
114
+ return {
115
+ ok: true,
116
+ reason: "timeout",
117
+ elapsed_ms,
118
+ until: untilVal,
119
+ run,
120
+ };
121
+ }
122
+ if (opts.onProgress &&
123
+ elapsed_ms - lastProgressAt >= PROGRESS_EVERY_MS) {
124
+ try {
125
+ const maybe = opts.onProgress({
126
+ progress: pollCount,
127
+ message: `waiting until=${untilVal} elapsed_ms=${elapsed_ms}`,
128
+ });
129
+ void Promise.resolve(maybe).catch(() => { });
130
+ }
131
+ catch {
132
+ }
133
+ lastProgressAt = elapsed_ms;
134
+ }
135
+ const remaining = clamped.value - elapsed_ms;
136
+ const sleepMs = Math.min(POLL_INTERVAL_MS, remaining);
137
+ try {
138
+ await sleepAbortable(sleepMs, signal);
139
+ }
140
+ catch {
141
+ return { ok: false, error: "wait aborted", code: "aborted" };
142
+ }
143
+ hadWaited = true;
144
+ pollCount += 1;
145
+ }
146
+ }
@@ -0,0 +1,12 @@
1
+ import type { RunStore, RunSummary, StageSnapshot } from "../runstore/port.js";
2
+ type WaitingSummarySource = Pick<RunSummary, "run_id" | "waiting_stage_id" | "waiting_stage_ids" | "waiting_kind" | "waiting_summary" | "waiting_prompt_id" | "waiting_artifacts" | "waiting_questions">;
3
+ export declare function projectWaitingGate(opts: {
4
+ runId: string;
5
+ stageId: string;
6
+ stage: StageSnapshot;
7
+ summary: WaitingSummarySource;
8
+ }): Record<string, unknown>;
9
+ export declare function projectWaitingGates(store: RunStore, opts?: {
10
+ runId?: string;
11
+ }): Promise<Array<Record<string, unknown>>>;
12
+ export {};
@@ -0,0 +1,83 @@
1
+ export function projectWaitingGate(opts) {
2
+ const { runId, stageId, stage, summary } = opts;
3
+ const prompt = stage.pending_prompt;
4
+ const item = {
5
+ runId,
6
+ stageId,
7
+ };
8
+ if (prompt !== undefined) {
9
+ item.waiting_kind = prompt.kind;
10
+ item.waiting_prompt_id = prompt.id;
11
+ item.pending_prompt = prompt;
12
+ if (prompt.kind === "multi_question") {
13
+ item.waiting_questions = prompt.questions.map((q) => q.message);
14
+ }
15
+ else {
16
+ item.waiting_summary = prompt.message;
17
+ }
18
+ if (prompt.kind === "artifact_backed") {
19
+ item.waiting_artifacts = prompt.artifacts;
20
+ }
21
+ }
22
+ else if (stageId === summary.waiting_stage_id) {
23
+ if (summary.waiting_kind !== undefined) {
24
+ item.waiting_kind = summary.waiting_kind;
25
+ }
26
+ if (summary.waiting_summary !== undefined) {
27
+ item.waiting_summary = summary.waiting_summary;
28
+ }
29
+ if (summary.waiting_prompt_id !== undefined) {
30
+ item.waiting_prompt_id = summary.waiting_prompt_id;
31
+ }
32
+ if (summary.waiting_artifacts !== undefined) {
33
+ item.waiting_artifacts = summary.waiting_artifacts;
34
+ }
35
+ if (summary.waiting_questions !== undefined) {
36
+ item.waiting_questions = summary.waiting_questions;
37
+ }
38
+ }
39
+ return item;
40
+ }
41
+ function projectGatesForRun(detail, summary) {
42
+ const stageIds = summary.waiting_stage_ids ??
43
+ (summary.waiting_stage_id !== undefined ? [summary.waiting_stage_id] : []);
44
+ const items = [];
45
+ for (const stageId of stageIds) {
46
+ const stage = detail.stages.find((s) => s.stage_id === stageId);
47
+ if (!stage || stage.status !== "waiting_for_input")
48
+ continue;
49
+ items.push(projectWaitingGate({
50
+ runId: summary.run_id,
51
+ stageId,
52
+ stage,
53
+ summary,
54
+ }));
55
+ }
56
+ return items;
57
+ }
58
+ export async function projectWaitingGates(store, opts) {
59
+ if (opts?.runId !== undefined) {
60
+ let detail;
61
+ try {
62
+ detail = await store.readRun(opts.runId);
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ return projectGatesForRun(detail, detail);
68
+ }
69
+ const summaries = await store.listRuns();
70
+ const waiting = summaries.filter((r) => (r.waiting_stage_ids !== undefined && r.waiting_stage_ids.length > 0) ||
71
+ r.waiting_stage_id !== undefined);
72
+ const perRun = await Promise.all(waiting.map(async (summary) => {
73
+ let detail;
74
+ try {
75
+ detail = await store.readRun(summary.run_id);
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ return projectGatesForRun(detail, summary);
81
+ }));
82
+ return perRun.flat();
83
+ }
@@ -0,0 +1,2 @@
1
+ export declare function findProjectRoot(startDir: string): string | null;
2
+ export declare function clearFindProjectRootCacheForTests(): void;