stageflow 0.3.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 (153) hide show
  1. package/README.md +22 -1
  2. package/dist/agent/activity.d.ts +2 -0
  3. package/dist/agent/cursorProvider.d.ts +1 -10
  4. package/dist/agent/cursorProvider.js +18 -10
  5. package/dist/agent/fakeAgent.js +11 -7
  6. package/dist/agent/piAdapter.d.ts +2 -1
  7. package/dist/agent/piAdapter.js +28 -25
  8. package/dist/agent/port.d.ts +6 -1
  9. package/dist/agent/port.js +3 -0
  10. package/dist/cli/artifactCommand.d.ts +10 -0
  11. package/dist/cli/artifactCommand.js +120 -0
  12. package/dist/cli/envelopeCommand.d.ts +32 -0
  13. package/dist/cli/envelopeCommand.js +285 -0
  14. package/dist/cli/exportRunCommand.d.ts +10 -0
  15. package/dist/cli/exportRunCommand.js +150 -0
  16. package/dist/cli/handoffFormat.d.ts +22 -0
  17. package/dist/cli/handoffFormat.js +61 -0
  18. package/dist/cli/runCommand.d.ts +5 -1
  19. package/dist/cli/runCommand.js +36 -5
  20. package/dist/cli/runOutput.d.ts +4 -1
  21. package/dist/cli/runOutput.js +36 -8
  22. package/dist/cli/skillsCommand.d.ts +16 -0
  23. package/dist/cli/skillsCommand.js +459 -0
  24. package/dist/cli.js +78 -5
  25. package/dist/config/loadPipeline.js +5 -1
  26. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  27. package/dist/config/normalizePipelineStageEntry.js +25 -1
  28. package/dist/config/pipelineStageKeys.js +1 -1
  29. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  30. package/dist/config/resolveForkEmitContext.js +23 -1
  31. package/dist/config/resolvePipelineDag.js +36 -4
  32. package/dist/envelope/check.d.ts +2 -0
  33. package/dist/envelope/check.js +51 -0
  34. package/dist/envelope/cloneForks.d.ts +2 -0
  35. package/dist/envelope/cloneForks.js +62 -0
  36. package/dist/envelope/forkChoice.js +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +2 -0
  39. package/dist/mcp/catalogTools.d.ts +3 -0
  40. package/dist/mcp/catalogTools.js +187 -0
  41. package/dist/mcp/controlTools.d.ts +3 -0
  42. package/dist/mcp/controlTools.js +147 -0
  43. package/dist/mcp/deps.d.ts +11 -0
  44. package/dist/mcp/deps.js +1 -0
  45. package/dist/mcp/projectRun.d.ts +1 -40
  46. package/dist/mcp/projectRun.js +1 -64
  47. package/dist/mcp/resources.d.ts +5 -0
  48. package/dist/mcp/resources.js +45 -0
  49. package/dist/mcp/server.d.ts +16 -3
  50. package/dist/mcp/server.js +159 -3
  51. package/dist/mcp/toolResults.d.ts +7 -0
  52. package/dist/mcp/toolResults.js +6 -0
  53. package/dist/mcp/tools.d.ts +2 -7
  54. package/dist/mcp/tools.js +52 -103
  55. package/dist/mcp/waitRun.d.ts +46 -0
  56. package/dist/mcp/waitRun.js +146 -0
  57. package/dist/mcp/waitingGates.d.ts +12 -0
  58. package/dist/mcp/waitingGates.js +83 -0
  59. package/dist/projection/projectRun.d.ts +44 -0
  60. package/dist/projection/projectRun.js +71 -0
  61. package/dist/prompt/priorEnvelope.d.ts +1 -1
  62. package/dist/prompt/priorEnvelope.js +4 -1
  63. package/dist/runstore/paths.js +2 -0
  64. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  65. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  66. package/dist/runstore/port.d.ts +13 -2
  67. package/dist/runstore/port.js +4 -1
  68. package/dist/runstore/runProjection.d.ts +1 -1
  69. package/dist/runstore/runProjection.js +14 -3
  70. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  71. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  72. package/dist/runstore/stageInstanceId.d.ts +4 -0
  73. package/dist/runstore/stageInstanceId.js +23 -0
  74. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  75. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  76. package/dist/runstore/trackProjection.js +28 -12
  77. package/dist/runstore/workspaceLayout.d.ts +1 -0
  78. package/dist/runstore/workspaceLayout.js +1 -1
  79. package/dist/runtime/cloneSchedule.d.ts +18 -0
  80. package/dist/runtime/cloneSchedule.js +233 -0
  81. package/dist/runtime/envelopeRouting.d.ts +2 -1
  82. package/dist/runtime/envelopeRouting.js +80 -13
  83. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  84. package/dist/runtime/pipelineScheduler.js +166 -30
  85. package/dist/runtime/resumeReconstruct.js +8 -3
  86. package/dist/runtime/runChangeBus.d.ts +18 -0
  87. package/dist/runtime/runChangeBus.js +83 -0
  88. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  89. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  90. package/dist/runtime/stageRunner.d.ts +1 -0
  91. package/dist/runtime/stageRunner.js +17 -15
  92. package/dist/runtime/stageWorker.js +11 -4
  93. package/dist/server/bootstrap.d.ts +32 -0
  94. package/dist/server/bootstrap.js +56 -0
  95. package/dist/server/createHttpHost.d.ts +34 -0
  96. package/dist/server/createHttpHost.js +70 -0
  97. package/dist/server/http.d.ts +6 -13
  98. package/dist/server/http.js +295 -374
  99. package/dist/server/mcpHost.d.ts +22 -0
  100. package/dist/server/mcpHost.js +21 -0
  101. package/dist/server/operatorResults.d.ts +13 -0
  102. package/dist/server/operatorResults.js +22 -0
  103. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  104. package/dist/tools/emitStageEnvelope.js +22 -6
  105. package/dist/types/envelope.d.ts +2 -0
  106. package/dist/types/forkChoice.d.ts +22 -0
  107. package/dist/types/pipeline.d.ts +8 -0
  108. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  109. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  110. package/dist/ui/index.html +2 -2
  111. package/package.json +2 -1
  112. package/skills/install-suite.sh +107 -0
  113. package/skills/stageflow/SKILL.md +26 -0
  114. package/skills/stageflow/references/control-surface.md +26 -0
  115. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  116. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  117. package/skills/stageflow-author/SKILL.md +58 -0
  118. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  119. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  120. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  121. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  122. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  123. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  124. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  125. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  126. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  127. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  128. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  130. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  131. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  132. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  133. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  134. package/skills/stageflow-delegate/SKILL.md +18 -0
  135. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  136. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  137. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  138. package/skills/stageflow-run/SKILL.md +151 -0
  139. package/skills/stageflow-run/references/mcp-call.md +17 -0
  140. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  141. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  142. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  143. package/skills/stageflow-session-capture/SKILL.md +94 -0
  144. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  145. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  146. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  147. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  148. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  149. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  150. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  151. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  152. package/skills/stageflow-setup/SKILL.md +110 -0
  153. package/dist/ui/assets/index-CFSzDZje.js +0 -118
@@ -0,0 +1,187 @@
1
+ import { z } from "zod";
2
+ import { browseCatalog } from "../config/browseCatalog.js";
3
+ import { loadPipeline } from "../config/loadPipeline.js";
4
+ import { validateCatalog } from "../config/validateCatalog.js";
5
+ import { projectRunForMcp } from "./projectRun.js";
6
+ import { readRunArtifact } from "./readArtifact.js";
7
+ import { textResult } from "./toolResults.js";
8
+ const taskFileSchema = z.object({
9
+ id: z.string(),
10
+ goal: z.string(),
11
+ context: z.string().optional(),
12
+ constraints: z.string().optional(),
13
+ checkout: z.string().optional(),
14
+ });
15
+ const startRunSchema = z
16
+ .object({
17
+ pipeline: z.string().describe("Filesystem path to a pipeline YAML file"),
18
+ task_path: z.string().optional(),
19
+ task: taskFileSchema.optional(),
20
+ })
21
+ .refine((data) => Boolean(data.task_path) !== Boolean(data.task), {
22
+ message: "Exactly one of task_path or task is required",
23
+ });
24
+ const runStatusSchema = z.enum(["created", "running", "succeeded", "failed"]);
25
+ export function registerCatalogTools(server, deps) {
26
+ const { manager, store, cwd } = deps;
27
+ server.registerTool("list_pipelines", {
28
+ description: "List manifest-declared pipeline paths from the project catalog",
29
+ inputSchema: z.object({}),
30
+ }, async () => {
31
+ const catalog = await browseCatalog(cwd);
32
+ return textResult({ pipelines: catalog.pipelines });
33
+ });
34
+ server.registerTool("list_tasks", {
35
+ description: "List manifest-declared task paths from the project catalog",
36
+ inputSchema: z.object({}),
37
+ }, async () => {
38
+ const catalog = await browseCatalog(cwd);
39
+ return textResult({ tasks: catalog.tasks });
40
+ });
41
+ server.registerTool("list_runs", {
42
+ description: "List known pipeline runs. Optional filters: status, since (ISO created_at lower bound), pipeline (id or path).",
43
+ inputSchema: z.object({
44
+ status: runStatusSchema.optional(),
45
+ since: z.string().optional(),
46
+ pipeline: z.string().optional(),
47
+ }),
48
+ }, async ({ status, since, pipeline }) => {
49
+ const filter = {};
50
+ if (status !== undefined)
51
+ filter.status = status;
52
+ if (since !== undefined) {
53
+ if (!Number.isFinite(Date.parse(since))) {
54
+ return textResult({ error: "since must be a valid date", status: 400 }, true);
55
+ }
56
+ filter.since = since;
57
+ }
58
+ if (pipeline !== undefined)
59
+ filter.pipeline = pipeline;
60
+ const runs = await store.listRuns(Object.keys(filter).length > 0 ? filter : undefined);
61
+ return textResult({ runs });
62
+ });
63
+ server.registerTool("get_health", {
64
+ 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.",
65
+ inputSchema: z.object({}),
66
+ }, async () => textResult(manager.getHealth()));
67
+ server.registerTool("start_run", {
68
+ description: "Start a pipeline run using a filesystem pipeline path and either task_path (catalog task file) or an inline task object. 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.",
69
+ inputSchema: startRunSchema,
70
+ }, async ({ pipeline, task_path, task }) => {
71
+ if (!pipeline.trim()) {
72
+ return textResult({ error: "pipeline is required" }, true);
73
+ }
74
+ const taskInput = task_path ?? task;
75
+ if (taskInput === undefined) {
76
+ return textResult({ error: "Exactly one of task_path or task is required" }, true);
77
+ }
78
+ const result = await manager.startRun({ pipeline, task: taskInput });
79
+ if (!result.ok) {
80
+ const { ok: _ok, reason, ...rest } = result;
81
+ return textResult({ error: reason, ...rest }, true);
82
+ }
83
+ return textResult({ runId: result.runId });
84
+ });
85
+ server.registerTool("get_run", {
86
+ description: "Poll a run: status, stage statuses, waiting_* / pending_prompt HITL fields, and envelope summary/payload/artifact paths (no events). Use list_stage_events / get_envelope for timelines and full envelopes.",
87
+ inputSchema: z.object({
88
+ runId: z.string(),
89
+ }),
90
+ }, async ({ runId }) => {
91
+ try {
92
+ const detail = await store.readRun(runId);
93
+ return textResult(projectRunForMcp(detail));
94
+ }
95
+ catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ const notFound = /not found|no such|unknown run/i.test(message);
98
+ return textResult({
99
+ error: message,
100
+ status: notFound ? 404 : 500,
101
+ }, true);
102
+ }
103
+ });
104
+ server.registerTool("read_artifact", {
105
+ description: "Read a text artifact from a run workspace by relative path (contained under the run workspace).",
106
+ inputSchema: z.object({
107
+ runId: z.string(),
108
+ path: z.string(),
109
+ }),
110
+ }, async ({ runId, path: artifactPath }) => {
111
+ try {
112
+ const content = await readRunArtifact(store, runId, artifactPath);
113
+ return textResult({ runId, path: artifactPath, content });
114
+ }
115
+ catch (err) {
116
+ const message = err instanceof Error ? err.message : String(err);
117
+ const notFound = message.startsWith("Run not found") ||
118
+ message.startsWith("Artifact not found") ||
119
+ /no such|not found/i.test(message);
120
+ return textResult({
121
+ error: message,
122
+ status: notFound ? 404 : 400,
123
+ }, true);
124
+ }
125
+ });
126
+ server.registerTool("validate", {
127
+ description: "Validate the project catalog (full), a pipeline path, or a task path. Returns ValidationResult JSON (ok, summary, findings).",
128
+ inputSchema: z.object({
129
+ pipeline: z.string().optional(),
130
+ task: z.string().optional(),
131
+ strict: z.boolean().optional(),
132
+ }),
133
+ }, async ({ pipeline, task, strict }) => {
134
+ try {
135
+ let scope = "full";
136
+ if (pipeline !== undefined && pipeline.trim())
137
+ scope = "pipeline";
138
+ else if (task !== undefined && task.trim())
139
+ scope = "task";
140
+ const result = await validateCatalog({
141
+ cwd,
142
+ scope,
143
+ ...(scope === "pipeline" ? { pipeline: pipeline.trim() } : {}),
144
+ ...(scope === "task" ? { task: task.trim() } : {}),
145
+ strict: strict ?? false,
146
+ });
147
+ return textResult(result);
148
+ }
149
+ catch (err) {
150
+ const message = err instanceof Error ? err.message : String(err);
151
+ return textResult({ error: message, status: 400 }, true);
152
+ }
153
+ });
154
+ server.registerTool("describe_pipeline", {
155
+ description: "Describe a pipeline DAG: stages with needs, fork, clonable, clone_cap, and gate_kinds. Input is a filesystem pipeline path (same as start_run).",
156
+ inputSchema: z.object({
157
+ pipeline: z.string(),
158
+ }),
159
+ }, async ({ pipeline }) => {
160
+ if (!pipeline.trim()) {
161
+ return textResult({ error: "pipeline is required", status: 400 }, true);
162
+ }
163
+ try {
164
+ const loaded = await loadPipeline(pipeline.trim(), { cwd });
165
+ const gateById = new Map(loaded.stages.map((s) => [s.id, s.gate_kinds]));
166
+ const stages = loaded.dag.nodes.map((node) => ({
167
+ id: node.id,
168
+ needs: node.needs,
169
+ ...(node.fork !== undefined ? { fork: node.fork } : {}),
170
+ ...(node.clonable !== undefined ? { clonable: node.clonable } : {}),
171
+ ...(node.clone_cap !== undefined ? { clone_cap: node.clone_cap } : {}),
172
+ ...(gateById.get(node.id) !== undefined
173
+ ? { gate_kinds: gateById.get(node.id) }
174
+ : {}),
175
+ }));
176
+ return textResult({
177
+ id: loaded.pipeline.id,
178
+ path: loaded.pipelinePath,
179
+ stages,
180
+ });
181
+ }
182
+ catch (err) {
183
+ const message = err instanceof Error ? err.message : String(err);
184
+ return textResult({ error: message, status: 404 }, true);
185
+ }
186
+ });
187
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { McpToolDeps } from "./deps.js";
3
+ export declare function registerControlTools(server: McpServer, deps: McpToolDeps): void;
@@ -0,0 +1,147 @@
1
+ import { z } from "zod";
2
+ import { mapRetryStageFailure, mapStartFailure, } from "../server/operatorResults.js";
3
+ import { parseAskOperatorAnswer } from "../tools/askOperator.js";
4
+ import { textResult } from "./toolResults.js";
5
+ import { projectWaitingGates } from "./waitingGates.js";
6
+ export function registerControlTools(server, deps) {
7
+ const { manager, store } = deps;
8
+ server.registerTool("list_waiting", {
9
+ description: "List stages waiting for operator input across runs (optional runId filter). Includes pending_prompt and waiting_* fields per gate.",
10
+ inputSchema: z.object({
11
+ runId: z.string().optional(),
12
+ }),
13
+ }, async ({ runId }) => {
14
+ const waiting = await projectWaitingGates(store, { runId });
15
+ return textResult({ waiting });
16
+ });
17
+ server.registerTool("answer_gate", {
18
+ description: "Deliver an AskOperatorAnswer for a stage in waiting_for_input (same semantics as POST /api/runs/:id/stages/:stageId/answer). Pass answer as { promptId, kind, ... } matching the pending prompt.",
19
+ inputSchema: z.object({
20
+ runId: z.string(),
21
+ stageId: z.string(),
22
+ answer: z.record(z.string(), z.unknown()),
23
+ }),
24
+ }, async ({ runId, stageId, answer }) => {
25
+ let parsed;
26
+ try {
27
+ parsed = parseAskOperatorAnswer(answer);
28
+ }
29
+ catch (err) {
30
+ const message = err instanceof Error ? err.message : String(err);
31
+ return textResult({ error: message, status: 400 }, true);
32
+ }
33
+ const result = await manager.deliverAnswer(runId, stageId, parsed);
34
+ if (!result.ok) {
35
+ return textResult({ error: result.reason, status: result.status }, true);
36
+ }
37
+ return textResult({ ok: true });
38
+ });
39
+ server.registerTool("list_stage_events", {
40
+ description: "List persisted stage log events for a run stage (optional attempt filter). Prefer over bloating get_run.",
41
+ inputSchema: z.object({
42
+ runId: z.string(),
43
+ stageId: z.string(),
44
+ attempt: z.number().int().positive().optional(),
45
+ }),
46
+ }, async ({ runId, stageId, attempt }) => {
47
+ try {
48
+ await store.readRunMeta(runId);
49
+ }
50
+ catch (err) {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ return textResult({ error: message, status: 404 }, true);
53
+ }
54
+ try {
55
+ const detail = await store.readRun(runId);
56
+ const stage = detail.stages.find((s) => s.stage_id === stageId);
57
+ if (!stage) {
58
+ return textResult({ error: `Stage not found: ${stageId}`, status: 404 }, true);
59
+ }
60
+ const events = await store.listStageEvents(runId, stageId, attempt);
61
+ return textResult({ runId, stageId, attempt, events });
62
+ }
63
+ catch (err) {
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ const notFound = /not found|no such/i.test(message);
66
+ return textResult({ error: message, status: notFound ? 404 : 500 }, true);
67
+ }
68
+ });
69
+ server.registerTool("get_envelope", {
70
+ description: "Read the full StageEnvelope for a run stage (latest attempt). Returns 404 when absent.",
71
+ inputSchema: z.object({
72
+ runId: z.string(),
73
+ stageId: z.string(),
74
+ }),
75
+ }, async ({ runId, stageId }) => {
76
+ try {
77
+ await store.readRunMeta(runId);
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ return textResult({ error: message, status: 404 }, true);
82
+ }
83
+ try {
84
+ const detail = await store.readRun(runId);
85
+ if (!detail.stages.some((s) => s.stage_id === stageId)) {
86
+ return textResult({ error: `Stage not found: ${stageId}`, status: 404 }, true);
87
+ }
88
+ const envelope = await store.readEnvelope(runId, stageId);
89
+ return textResult({ runId, stageId, envelope });
90
+ }
91
+ catch (err) {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ const notFound = /not found|no such|envelope/i.test(message);
94
+ return textResult({ error: message, status: notFound ? 404 : 500 }, true);
95
+ }
96
+ });
97
+ server.registerTool("retry_stage", {
98
+ description: "Retry a failed stage (same as POST .../retry). Waiting stages cannot be retried — answer them with answer_gate instead.",
99
+ inputSchema: z.object({
100
+ runId: z.string(),
101
+ stageId: z.string(),
102
+ }),
103
+ }, async ({ runId, stageId }) => {
104
+ const result = await manager.retryStage(runId, stageId);
105
+ if (!result.ok) {
106
+ return textResult({ ...mapRetryStageFailure(result), status: result.status }, true);
107
+ }
108
+ return textResult({
109
+ runId: result.runId,
110
+ stageId: result.stageId,
111
+ attemptIndex: result.attemptIndex,
112
+ });
113
+ });
114
+ server.registerTool("abandon_stage", {
115
+ description: "Abandon a running stage (marks it failed/interrupted). Does not dismiss HITL — waiting stages return 409; answer them with answer_gate. There is no run-level cancel tool.",
116
+ inputSchema: z.object({
117
+ runId: z.string(),
118
+ stageId: z.string(),
119
+ }),
120
+ }, async ({ runId, stageId }) => {
121
+ const result = await manager.abandonStage(runId, stageId);
122
+ if (!result.ok) {
123
+ const fail = result;
124
+ return textResult({ error: fail.reason, status: fail.status }, true);
125
+ }
126
+ return textResult({
127
+ ok: true,
128
+ runId: result.runId,
129
+ stageId: result.stageId,
130
+ });
131
+ });
132
+ server.registerTool("rerun", {
133
+ description: "Start a new run from a completed or failed run's pipeline/task locators. Returns { runId } for the new run.",
134
+ inputSchema: z.object({
135
+ runId: z.string(),
136
+ }),
137
+ }, async ({ runId }) => {
138
+ const result = await manager.rerun(runId);
139
+ if (!result.ok) {
140
+ return textResult({
141
+ ...mapStartFailure(result),
142
+ ...(result.status !== undefined ? { status: result.status } : {}),
143
+ }, true);
144
+ }
145
+ return textResult({ runId: result.runId });
146
+ });
147
+ }
@@ -0,0 +1,11 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { RunChangeBus } from "../runtime/runChangeBus.js";
3
+ import type { RunManager } from "../runtime/runManager.js";
4
+ export type McpToolDeps = {
5
+ manager: RunManager;
6
+ store: RunStore;
7
+ cwd: string;
8
+ };
9
+ export type McpHttpDeps = McpToolDeps & {
10
+ runChangeBus: RunChangeBus;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,40 +1 @@
1
- import type { RunDetail, StageSnapshot } from "../runstore/port.js";
2
- import type { PipelineTrackProjection, RunSummary } from "../runstore/port.js";
3
- import type { AskOperatorPrompt } from "../tools/askOperator.js";
4
- import type { StageEnvelope } from "../types/envelope.js";
5
- export type McpStageProjection = {
6
- stage_id: string;
7
- status: StageSnapshot["status"];
8
- envelope: {
9
- status: StageEnvelope["status"];
10
- summary: string;
11
- artifacts: string[];
12
- payload?: Record<string, unknown>;
13
- } | null;
14
- artifacts: string[];
15
- last_at?: string;
16
- pending_prompt?: AskOperatorPrompt;
17
- };
18
- export type McpRunProjection = {
19
- run_id: string;
20
- pipeline_id: string;
21
- task_id?: string;
22
- pipeline_path?: string;
23
- task_path?: string;
24
- project_root?: string;
25
- status: RunDetail["status"];
26
- created_at: string;
27
- updated_at?: string;
28
- stages: McpStageProjection[];
29
- pipeline_track: PipelineTrackProjection;
30
- waiting_stage_id?: RunSummary["waiting_stage_id"];
31
- waiting_stage_ids?: RunSummary["waiting_stage_ids"];
32
- waiting_summary?: RunSummary["waiting_summary"];
33
- waiting_kind?: RunSummary["waiting_kind"];
34
- waiting_prompt_id?: RunSummary["waiting_prompt_id"];
35
- waiting_artifacts?: RunSummary["waiting_artifacts"];
36
- waiting_questions?: RunSummary["waiting_questions"];
37
- failed_stage_id?: RunSummary["failed_stage_id"];
38
- failed_reason?: RunSummary["failed_reason"];
39
- };
40
- export declare function projectRunForMcp(detail: RunDetail): McpRunProjection;
1
+ export { projectRun as projectRunForMcp, type RunProjection as McpRunProjection, type StageProjection as McpStageProjection, } from "../projection/projectRun.js";
@@ -1,64 +1 @@
1
- export function projectRunForMcp(detail) {
2
- return {
3
- run_id: detail.run_id,
4
- pipeline_id: detail.pipeline_id,
5
- task_id: detail.task_id,
6
- ...(detail.pipeline_path !== undefined
7
- ? { pipeline_path: detail.pipeline_path }
8
- : {}),
9
- ...(detail.task_path !== undefined ? { task_path: detail.task_path } : {}),
10
- ...(detail.project_root !== undefined
11
- ? { project_root: detail.project_root }
12
- : {}),
13
- status: detail.status,
14
- created_at: detail.created_at,
15
- updated_at: detail.updated_at,
16
- pipeline_track: detail.pipeline_track,
17
- ...(detail.waiting_stage_id !== undefined
18
- ? { waiting_stage_id: detail.waiting_stage_id }
19
- : {}),
20
- ...(detail.waiting_stage_ids !== undefined
21
- ? { waiting_stage_ids: detail.waiting_stage_ids }
22
- : {}),
23
- ...(detail.waiting_summary !== undefined
24
- ? { waiting_summary: detail.waiting_summary }
25
- : {}),
26
- ...(detail.waiting_kind !== undefined
27
- ? { waiting_kind: detail.waiting_kind }
28
- : {}),
29
- ...(detail.waiting_prompt_id !== undefined
30
- ? { waiting_prompt_id: detail.waiting_prompt_id }
31
- : {}),
32
- ...(detail.waiting_artifacts !== undefined
33
- ? { waiting_artifacts: detail.waiting_artifacts }
34
- : {}),
35
- ...(detail.waiting_questions !== undefined
36
- ? { waiting_questions: detail.waiting_questions }
37
- : {}),
38
- ...(detail.failed_stage_id !== undefined
39
- ? { failed_stage_id: detail.failed_stage_id }
40
- : {}),
41
- ...(detail.failed_reason !== undefined
42
- ? { failed_reason: detail.failed_reason }
43
- : {}),
44
- stages: detail.stages.map((stage) => ({
45
- stage_id: stage.stage_id,
46
- status: stage.status,
47
- envelope: stage.envelope
48
- ? {
49
- status: stage.envelope.status,
50
- summary: stage.envelope.summary,
51
- artifacts: stage.envelope.artifacts,
52
- ...(stage.envelope.payload !== undefined
53
- ? { payload: stage.envelope.payload }
54
- : {}),
55
- }
56
- : null,
57
- artifacts: stage.artifacts,
58
- last_at: stage.last_at,
59
- ...(stage.pending_prompt
60
- ? { pending_prompt: stage.pending_prompt }
61
- : {}),
62
- })),
63
- };
64
- }
1
+ export { projectRun as projectRunForMcp, } from "../projection/projectRun.js";
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import type { McpToolDeps } from "./deps.js";
3
+ export declare const RUN_RESOURCE_URI_TEMPLATE = "stageflow://runs/{runId}";
4
+ export declare function runResourceUri(runId: string): string;
5
+ export declare function registerRunResources(server: McpServer, deps: McpToolDeps): void;
@@ -0,0 +1,45 @@
1
+ import { ResourceNotFoundError, ResourceTemplate, } from "@modelcontextprotocol/server";
2
+ import { projectRunForMcp } from "./projectRun.js";
3
+ export const RUN_RESOURCE_URI_TEMPLATE = "stageflow://runs/{runId}";
4
+ export function runResourceUri(runId) {
5
+ return `stageflow://runs/${runId}`;
6
+ }
7
+ export function registerRunResources(server, deps) {
8
+ const { store } = deps;
9
+ server.registerResource("run", new ResourceTemplate(RUN_RESOURCE_URI_TEMPLATE, {
10
+ list: async () => {
11
+ const runs = await store.listRuns();
12
+ return {
13
+ resources: runs.map((run) => ({
14
+ uri: runResourceUri(run.run_id),
15
+ name: run.run_id,
16
+ mimeType: "application/json",
17
+ })),
18
+ };
19
+ },
20
+ }), {
21
+ description: "Lean Stageflow run projection (same shape as get_run; no stage events)",
22
+ mimeType: "application/json",
23
+ }, async (uri, variables) => {
24
+ const runId = variables.runId;
25
+ if (typeof runId !== "string" || runId.length === 0) {
26
+ throw new ResourceNotFoundError(uri.href);
27
+ }
28
+ try {
29
+ const detail = await store.readRun(runId);
30
+ const projected = projectRunForMcp(detail);
31
+ return {
32
+ contents: [
33
+ {
34
+ uri: uri.href,
35
+ mimeType: "application/json",
36
+ text: JSON.stringify(projected, null, 2),
37
+ },
38
+ ],
39
+ };
40
+ }
41
+ catch {
42
+ throw new ResourceNotFoundError(uri.href);
43
+ }
44
+ });
45
+ }
@@ -1,7 +1,20 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { McpServer } from "@modelcontextprotocol/server";
3
- import { type McpToolDeps } from "./tools.js";
4
- export type { McpToolDeps };
3
+ import type { McpHttpDeps, McpToolDeps } from "./deps.js";
4
+ export type { McpHttpDeps, McpToolDeps };
5
+ export type McpHttpOptions = {
6
+ /** Test/debug escape hatch — per-request create/teardown (no sessions). */
7
+ mcpStateless?: boolean;
8
+ };
5
9
  export declare function createStageflowMcpServer(deps: McpToolDeps): McpServer;
6
- /** Stateless Streamable HTTP: new server + transport per request. */
10
+ export type McpHttpHandler = {
11
+ handle: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
12
+ close: () => Promise<void>;
13
+ };
14
+ /** Stateful Streamable HTTP session host (product default) or stateless escape hatch. */
15
+ export declare function createMcpHttpHandler(deps: McpHttpDeps, options?: McpHttpOptions): McpHttpHandler;
7
16
  export declare function handleMcpHttpRequest(req: IncomingMessage, res: ServerResponse, deps: McpToolDeps): Promise<void>;
17
+ export declare function resolveMcpStateless(options?: {
18
+ mcpStateless?: boolean;
19
+ env?: NodeJS.ProcessEnv;
20
+ }): boolean;