stageflow 0.3.0 → 0.9.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 (164) hide show
  1. package/README.md +44 -14
  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/runsCommand.d.ts +22 -0
  23. package/dist/cli/runsCommand.js +591 -0
  24. package/dist/cli/skillsCommand.d.ts +16 -0
  25. package/dist/cli/skillsCommand.js +459 -0
  26. package/dist/cli/validateOutput.d.ts +2 -2
  27. package/dist/cli/validateOutput.js +2 -2
  28. package/dist/cli.js +110 -5
  29. package/dist/config/loadPipeline.js +5 -1
  30. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  31. package/dist/config/normalizePipelineStageEntry.js +25 -1
  32. package/dist/config/pipelineStageKeys.js +1 -1
  33. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  34. package/dist/config/resolveForkEmitContext.js +23 -1
  35. package/dist/config/resolvePipelineDag.js +36 -4
  36. package/dist/envelope/check.d.ts +2 -0
  37. package/dist/envelope/check.js +51 -0
  38. package/dist/envelope/cloneForks.d.ts +2 -0
  39. package/dist/envelope/cloneForks.js +62 -0
  40. package/dist/envelope/forkChoice.js +1 -1
  41. package/dist/index.d.ts +3 -1
  42. package/dist/index.js +3 -1
  43. package/dist/mcp/catalogTools.d.ts +3 -0
  44. package/dist/mcp/catalogTools.js +187 -0
  45. package/dist/mcp/controlTools.d.ts +3 -0
  46. package/dist/mcp/controlTools.js +147 -0
  47. package/dist/mcp/deps.d.ts +11 -0
  48. package/dist/mcp/deps.js +1 -0
  49. package/dist/mcp/projectRun.d.ts +1 -40
  50. package/dist/mcp/projectRun.js +1 -64
  51. package/dist/mcp/resources.d.ts +5 -0
  52. package/dist/mcp/resources.js +45 -0
  53. package/dist/mcp/server.d.ts +16 -3
  54. package/dist/mcp/server.js +159 -3
  55. package/dist/mcp/toolResults.d.ts +7 -0
  56. package/dist/mcp/toolResults.js +6 -0
  57. package/dist/mcp/tools.d.ts +2 -7
  58. package/dist/mcp/tools.js +52 -103
  59. package/dist/mcp/waitRun.d.ts +46 -0
  60. package/dist/mcp/waitRun.js +146 -0
  61. package/dist/mcp/waitingGates.d.ts +12 -0
  62. package/dist/mcp/waitingGates.js +83 -0
  63. package/dist/package-meta.d.ts +1 -0
  64. package/dist/package-meta.js +1 -0
  65. package/dist/projection/projectRun.d.ts +44 -0
  66. package/dist/projection/projectRun.js +71 -0
  67. package/dist/prompt/priorEnvelope.d.ts +1 -1
  68. package/dist/prompt/priorEnvelope.js +4 -1
  69. package/dist/runstore/paths.js +2 -0
  70. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  71. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  72. package/dist/runstore/port.d.ts +13 -2
  73. package/dist/runstore/port.js +4 -1
  74. package/dist/runstore/runProjection.d.ts +1 -1
  75. package/dist/runstore/runProjection.js +14 -3
  76. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  77. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  78. package/dist/runstore/stageInstanceId.d.ts +4 -0
  79. package/dist/runstore/stageInstanceId.js +23 -0
  80. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  81. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  82. package/dist/runstore/trackProjection.js +28 -12
  83. package/dist/runstore/workspaceLayout.d.ts +1 -0
  84. package/dist/runstore/workspaceLayout.js +1 -1
  85. package/dist/runtime/cloneSchedule.d.ts +18 -0
  86. package/dist/runtime/cloneSchedule.js +233 -0
  87. package/dist/runtime/envelopeRouting.d.ts +2 -1
  88. package/dist/runtime/envelopeRouting.js +80 -13
  89. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  90. package/dist/runtime/pipelineScheduler.js +166 -30
  91. package/dist/runtime/resumeReconstruct.js +8 -3
  92. package/dist/runtime/runChangeBus.d.ts +18 -0
  93. package/dist/runtime/runChangeBus.js +83 -0
  94. package/dist/runtime/runManager.d.ts +7 -0
  95. package/dist/runtime/runManager.js +31 -2
  96. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  97. package/dist/runtime/runRetryCoordinator.js +4 -1
  98. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  99. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  100. package/dist/runtime/stageRunner.d.ts +1 -0
  101. package/dist/runtime/stageRunner.js +17 -15
  102. package/dist/runtime/stageWorker.js +11 -4
  103. package/dist/server/bootstrap.d.ts +32 -0
  104. package/dist/server/bootstrap.js +56 -0
  105. package/dist/server/createHttpHost.d.ts +34 -0
  106. package/dist/server/createHttpHost.js +70 -0
  107. package/dist/server/http.d.ts +6 -13
  108. package/dist/server/http.js +295 -374
  109. package/dist/server/mcpHost.d.ts +22 -0
  110. package/dist/server/mcpHost.js +21 -0
  111. package/dist/server/operatorResults.d.ts +13 -0
  112. package/dist/server/operatorResults.js +22 -0
  113. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  114. package/dist/tools/emitStageEnvelope.js +22 -6
  115. package/dist/types/envelope.d.ts +2 -0
  116. package/dist/types/forkChoice.d.ts +22 -0
  117. package/dist/types/pipeline.d.ts +8 -0
  118. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  119. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  120. package/dist/ui/index.html +2 -2
  121. package/package.json +2 -1
  122. package/skills/install-suite.sh +107 -0
  123. package/skills/stageflow/SKILL.md +26 -0
  124. package/skills/stageflow/references/control-surface.md +28 -0
  125. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  126. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  127. package/skills/stageflow-author/SKILL.md +58 -0
  128. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  130. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  131. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  132. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  133. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  134. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  135. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  136. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  137. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  138. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  139. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  140. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  141. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  142. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  143. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  144. package/skills/stageflow-delegate/SKILL.md +18 -0
  145. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  146. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  147. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  148. package/skills/stageflow-run/SKILL.md +182 -0
  149. package/skills/stageflow-run/references/mcp-call.md +17 -0
  150. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  151. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  152. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  153. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  154. package/skills/stageflow-session-capture/SKILL.md +94 -0
  155. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  156. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  157. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  158. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  159. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  160. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  161. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  162. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  163. package/skills/stageflow-setup/SKILL.md +110 -0
  164. package/dist/ui/assets/index-CFSzDZje.js +0 -118
package/dist/mcp/tools.js CHANGED
@@ -1,113 +1,62 @@
1
1
  import { z } from "zod";
2
- import { browseCatalog } from "../config/browseCatalog.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
- const startRunSchema = z
13
- .object({
14
- pipeline: z.string().describe("Filesystem path to a pipeline YAML file"),
15
- task_path: z.string().optional(),
16
- task: taskFileSchema.optional(),
17
- })
18
- .refine((data) => Boolean(data.task_path) !== Boolean(data.task), {
19
- message: "Exactly one of task_path or task is required",
20
- });
21
- function textResult(data, isError = false) {
22
- return {
23
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
24
- ...(isError ? { isError: true } : {}),
25
- };
26
- }
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";
27
6
  export function registerMcpTools(server, deps) {
28
- const { manager, store, cwd } = deps;
29
- server.registerTool("list_pipelines", {
30
- description: "List manifest-declared pipeline paths from the project catalog",
31
- inputSchema: z.object({}),
32
- }, async () => {
33
- const catalog = await browseCatalog(cwd);
34
- return textResult({ pipelines: catalog.pipelines });
35
- });
36
- server.registerTool("list_tasks", {
37
- description: "List manifest-declared task paths from the project catalog",
38
- inputSchema: z.object({}),
39
- }, async () => {
40
- const catalog = await browseCatalog(cwd);
41
- return textResult({ tasks: catalog.tasks });
42
- });
43
- server.registerTool("list_runs", {
44
- description: "List known pipeline runs",
45
- inputSchema: z.object({}),
46
- }, async () => {
47
- const runs = await store.listRuns();
48
- return textResult({ runs });
49
- });
50
- server.registerTool("get_health", {
51
- 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.",
52
- inputSchema: z.object({}),
53
- }, async () => textResult(manager.getHealth()));
54
- server.registerTool("start_run", {
55
- 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.",
56
- inputSchema: startRunSchema,
57
- }, async ({ pipeline, task_path, task }) => {
58
- if (!pipeline.trim()) {
59
- return textResult({ error: "pipeline is required" }, true);
60
- }
61
- const taskInput = task_path ?? task;
62
- if (taskInput === undefined) {
63
- return textResult({ error: "Exactly one of task_path or task is required" }, true);
64
- }
65
- const result = await manager.startRun({ pipeline, task: taskInput });
66
- if (!result.ok) {
67
- const { ok: _ok, reason, ...rest } = result;
68
- return textResult({ error: reason, ...rest }, true);
69
- }
70
- return textResult({ runId: result.runId });
71
- });
72
- server.registerTool("get_run", {
73
- 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.",
74
12
  inputSchema: z.object({
75
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"'),
76
22
  }),
77
- }, async ({ runId }) => {
78
- try {
79
- const detail = await store.readRun(runId);
80
- return textResult(projectRunForMcp(detail));
81
- }
82
- catch (err) {
83
- const message = err instanceof Error ? err.message : String(err);
84
- const notFound = /not found|no such|unknown run/i.test(message);
85
- return textResult({
86
- error: message,
87
- status: notFound ? 404 : 500,
88
- }, true);
89
- }
90
- });
91
- server.registerTool("read_artifact", {
92
- description: "Read a text artifact from a run workspace by relative path (contained under the run workspace).",
93
- inputSchema: z.object({
94
- runId: z.string(),
95
- path: z.string(),
96
- }),
97
- }, async ({ runId, path: artifactPath }) => {
98
- try {
99
- const content = await readRunArtifact(store, runId, artifactPath);
100
- return textResult({ runId, path: artifactPath, content });
101
- }
102
- catch (err) {
103
- const message = err instanceof Error ? err.message : String(err);
104
- const notFound = message.startsWith("Run not found") ||
105
- message.startsWith("Artifact not found") ||
106
- /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) {
107
49
  return textResult({
108
- error: message,
109
- status: notFound ? 404 : 400,
50
+ error: result.error,
51
+ ...(result.status !== undefined ? { status: result.status } : {}),
52
+ ...(result.code !== undefined ? { code: result.code } : {}),
110
53
  }, true);
111
54
  }
55
+ return textResult({
56
+ reason: result.reason,
57
+ elapsed_ms: result.elapsed_ms,
58
+ until: result.until,
59
+ run: result.run,
60
+ });
112
61
  });
113
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
+ }
@@ -1 +1,2 @@
1
1
  export declare const PACKAGE_NAME = "stageflow";
2
+ export declare const PACKAGE_VERSION = "0.9.0";
@@ -1 +1,2 @@
1
1
  export const PACKAGE_NAME = "stageflow";
2
+ export const PACKAGE_VERSION = "0.9.0";
@@ -0,0 +1,44 @@
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
+ import type { CloneForkItem } from "../types/forkChoice.js";
6
+ export type StageProjection = {
7
+ stage_id: string;
8
+ status: StageSnapshot["status"];
9
+ envelope: {
10
+ status: StageEnvelope["status"];
11
+ summary: string;
12
+ artifacts: string[];
13
+ payload?: Record<string, unknown>;
14
+ fork_choice?: string[];
15
+ clone_forks?: CloneForkItem[];
16
+ } | null;
17
+ artifacts: string[];
18
+ last_at?: string;
19
+ pending_prompt?: AskOperatorPrompt;
20
+ };
21
+ export type RunProjection = {
22
+ run_id: string;
23
+ pipeline_id: string;
24
+ task_id?: string;
25
+ pipeline_path?: string;
26
+ task_path?: string;
27
+ project_root?: string;
28
+ status: RunDetail["status"];
29
+ created_at: string;
30
+ updated_at?: string;
31
+ stages: StageProjection[];
32
+ pipeline_track: PipelineTrackProjection;
33
+ waiting_stage_id?: RunSummary["waiting_stage_id"];
34
+ waiting_stage_ids?: RunSummary["waiting_stage_ids"];
35
+ waiting_summary?: RunSummary["waiting_summary"];
36
+ waiting_kind?: RunSummary["waiting_kind"];
37
+ waiting_prompt_id?: RunSummary["waiting_prompt_id"];
38
+ waiting_artifacts?: RunSummary["waiting_artifacts"];
39
+ waiting_questions?: RunSummary["waiting_questions"];
40
+ failed_stage_id?: RunSummary["failed_stage_id"];
41
+ failed_reason?: RunSummary["failed_reason"];
42
+ };
43
+ export declare function projectRun(detail: RunDetail): RunProjection;
44
+ export { projectRun as projectRunForMcp };
@@ -0,0 +1,71 @@
1
+ export function projectRun(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
+ ...(stage.envelope.fork_choice !== undefined
56
+ ? { fork_choice: stage.envelope.fork_choice }
57
+ : {}),
58
+ ...(stage.envelope.clone_forks !== undefined
59
+ ? { clone_forks: stage.envelope.clone_forks }
60
+ : {}),
61
+ }
62
+ : null,
63
+ artifacts: stage.artifacts,
64
+ last_at: stage.last_at,
65
+ ...(stage.pending_prompt
66
+ ? { pending_prompt: stage.pending_prompt }
67
+ : {}),
68
+ })),
69
+ };
70
+ }
71
+ export { projectRun as projectRunForMcp };
@@ -1,2 +1,2 @@
1
1
  import type { StageEnvelope } from "../types/envelope.js";
2
- export declare function formatPriorEnvelope(priorEnvelope: StageEnvelope | null): string;
2
+ export declare function formatPriorEnvelope(priorEnvelope: StageEnvelope | null, priorEnvelopes?: StageEnvelope[]): string;
@@ -1,4 +1,7 @@
1
- export function formatPriorEnvelope(priorEnvelope) {
1
+ export function formatPriorEnvelope(priorEnvelope, priorEnvelopes) {
2
+ if (priorEnvelopes !== undefined && priorEnvelopes.length > 0) {
3
+ return `Prior envelopes (${priorEnvelopes.length} clones, clone-list order):\n${JSON.stringify(priorEnvelopes, null, 2)}`;
4
+ }
2
5
  return priorEnvelope === null
3
6
  ? "No prior envelope (first stage)."
4
7
  : `Prior envelope JSON:\n${JSON.stringify(priorEnvelope, null, 2)}`;
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { readdirSync, renameSync, rmdirSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { guardStageId } from "./workspaceLayout.js";
4
5
  export function newRunId() {
5
6
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
6
7
  return `${stamp}-${randomBytes(3).toString("hex")}`;
@@ -56,5 +57,6 @@ export function runWorkspaceDir(storeRoot, runId) {
56
57
  return path.join(runsDir(storeRoot), runId);
57
58
  }
58
59
  export function stageDir(workspaceDir, stageId) {
60
+ guardStageId(stageId);
59
61
  return path.join(workspaceDir, "stages", stageId);
60
62
  }
@@ -3,3 +3,12 @@ import type { RunPipelineDagSnapshot } from "./port.js";
3
3
  export declare function buildPipelineDagSnapshotFromLoaded(loaded: LoadedPipeline): RunPipelineDagSnapshot;
4
4
  export declare function linearCompatDagSnapshot(stageIds: string[]): RunPipelineDagSnapshot;
5
5
  export declare function parsePipelineDagSnapshot(raw: unknown): RunPipelineDagSnapshot | null;
6
+ export declare function instancesOfDefinition(snapshot: RunPipelineDagSnapshot, catalogId: string): string[];
7
+ export declare function appendCloneInstances(snapshot: RunPipelineDagSnapshot, input: {
8
+ catalogId: string;
9
+ predecessorId: string;
10
+ count: number;
11
+ }): {
12
+ snapshot: RunPipelineDagSnapshot;
13
+ instanceIds: string[];
14
+ };