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
@@ -1,3 +1,4 @@
1
+ import { mintCloneInstanceIds } from "./stageInstanceId.js";
1
2
  export function buildPipelineDagSnapshotFromLoaded(loaded) {
2
3
  const gate_kinds = {};
3
4
  for (const stage of loaded.stages) {
@@ -7,7 +8,10 @@ export function buildPipelineDagSnapshotFromLoaded(loaded) {
7
8
  }
8
9
  return {
9
10
  stage_ids: [...loaded.pipeline.stages],
10
- nodes: loaded.dag.nodes,
11
+ nodes: loaded.dag.nodes.map((node) => ({
12
+ ...node,
13
+ definition_id: node.id,
14
+ })),
11
15
  roots: loaded.dag.roots,
12
16
  childrenOf: loaded.dag.childrenOf,
13
17
  ...(Object.keys(gate_kinds).length > 0 ? { gate_kinds } : {}),
@@ -19,6 +23,7 @@ export function linearCompatDagSnapshot(stageIds) {
19
23
  needs: index === 0 ? null : stageIds[index - 1],
20
24
  ancestors: index === 0 ? [] : stageIds.slice(0, index),
21
25
  stageIndex: index,
26
+ definition_id: id,
22
27
  }));
23
28
  const childrenOf = {};
24
29
  for (let i = 1; i < stageIds.length; i++) {
@@ -41,3 +46,87 @@ export function parsePipelineDagSnapshot(raw) {
41
46
  return null;
42
47
  return raw;
43
48
  }
49
+ export function instancesOfDefinition(snapshot, catalogId) {
50
+ const nodeById = new Map(snapshot.nodes.map((n) => [n.id, n]));
51
+ return snapshot.stage_ids.filter((id) => {
52
+ const node = nodeById.get(id);
53
+ return (node?.definition_id ?? node?.id) === catalogId;
54
+ });
55
+ }
56
+ export function appendCloneInstances(snapshot, input) {
57
+ const { catalogId, predecessorId, count } = input;
58
+ if (!snapshot.stage_ids.includes(catalogId)) {
59
+ throw new Error(`catalog stage "${catalogId}" is not in the run DAG snapshot`);
60
+ }
61
+ const predNode = snapshot.nodes.find((n) => n.id === predecessorId);
62
+ if (!predNode) {
63
+ throw new Error(`predecessor "${predecessorId}" is not in the run DAG snapshot`);
64
+ }
65
+ const instanceIds = mintCloneInstanceIds(catalogId, count);
66
+ for (const id of instanceIds) {
67
+ if (snapshot.stage_ids.includes(id) ||
68
+ snapshot.nodes.some((n) => n.id === id)) {
69
+ throw new Error(`instance id "${id}" already exists in the run DAG snapshot`);
70
+ }
71
+ }
72
+ const catalogNode = snapshot.nodes.find((n) => n.id === catalogId);
73
+ if (!catalogNode) {
74
+ throw new Error(`catalog stage "${catalogId}" is not in the run DAG snapshot`);
75
+ }
76
+ const catalogIdx = snapshot.stage_ids.indexOf(catalogId);
77
+ const stage_ids = [
78
+ ...snapshot.stage_ids.slice(0, catalogIdx),
79
+ ...instanceIds,
80
+ ...snapshot.stage_ids.slice(catalogIdx + 1),
81
+ ];
82
+ const instanceNodes = instanceIds.map((id) => ({
83
+ id,
84
+ definition_id: catalogId,
85
+ needs: predecessorId,
86
+ ancestors: [...predNode.ancestors, predecessorId],
87
+ stageIndex: 0,
88
+ ...(catalogNode.fork !== undefined ? { fork: catalogNode.fork } : {}),
89
+ }));
90
+ const remaining = new Map(snapshot.nodes
91
+ .filter((n) => n.id !== catalogId)
92
+ .map((n) => [n.id, n]));
93
+ const minted = new Map(instanceNodes.map((n) => [n.id, n]));
94
+ const nodes = stage_ids.map((id, index) => {
95
+ const node = minted.get(id) ?? remaining.get(id);
96
+ if (!node) {
97
+ throw new Error(`missing node for "${id}" after clone fan-out`);
98
+ }
99
+ return { ...node, stageIndex: index };
100
+ });
101
+ const childrenOf = {};
102
+ for (const [key, children] of Object.entries(snapshot.childrenOf)) {
103
+ childrenOf[key] = [...children];
104
+ }
105
+ const predChildren = childrenOf[predecessorId] ?? [];
106
+ const predIdx = predChildren.indexOf(catalogId);
107
+ childrenOf[predecessorId] =
108
+ predIdx === -1
109
+ ? [...predChildren, ...instanceIds]
110
+ : [
111
+ ...predChildren.slice(0, predIdx),
112
+ ...instanceIds,
113
+ ...predChildren.slice(predIdx + 1),
114
+ ];
115
+ const definitionParent = catalogNode.needs;
116
+ if (definitionParent && definitionParent !== predecessorId) {
117
+ const parentChildren = childrenOf[definitionParent] ?? [];
118
+ childrenOf[definitionParent] = parentChildren.filter((id) => id !== catalogId);
119
+ if (childrenOf[definitionParent].length === 0) {
120
+ delete childrenOf[definitionParent];
121
+ }
122
+ }
123
+ return {
124
+ snapshot: {
125
+ ...snapshot,
126
+ stage_ids,
127
+ nodes,
128
+ childrenOf,
129
+ },
130
+ instanceIds,
131
+ };
132
+ }
@@ -18,6 +18,7 @@ export type PipelineTrackNode = {
18
18
  blocked_by?: string[];
19
19
  gate_kinds?: StageGateKind[];
20
20
  attempt_count?: number;
21
+ definition_id?: string;
21
22
  };
22
23
  export type PipelineTrackEdge = {
23
24
  from: string;
@@ -55,7 +56,8 @@ export type StageLogEvent = StageLogLine & {
55
56
  };
56
57
  export type StageSnapshot = {
57
58
  stage_id: string;
58
- status: "pending" | "running" | "waiting_for_input" | "succeeded" | "failed";
59
+ definition_id?: string;
60
+ status: "pending" | "running" | "waiting_for_input" | "succeeded" | "failed" | "skipped";
59
61
  events: StageLogEvent[];
60
62
  envelope: StageEnvelope | null;
61
63
  artifacts: string[];
@@ -82,6 +84,7 @@ export type CompactStage = {
82
84
  id: string;
83
85
  status: StageSnapshot["status"];
84
86
  attempt_count: number;
87
+ definition_id?: string;
85
88
  };
86
89
  export type RunSummary = {
87
90
  run_id: string;
@@ -125,6 +128,13 @@ export type CreateRunInput = {
125
128
  taskPath?: string;
126
129
  projectRoot?: string;
127
130
  };
131
+ export type ListRunsFilter = {
132
+ status?: RunStatus;
133
+ /** ISO timestamp; keep runs with created_at >= since */
134
+ since?: string;
135
+ /** Match pipeline_id or pipeline_path */
136
+ pipeline?: string;
137
+ };
128
138
  /**
129
139
  * Persistence port for pipeline run state.
130
140
  * Call sites should prefer CreatedRun.workspaceDir from createRun over
@@ -154,8 +164,9 @@ export interface RunStore {
154
164
  attempt?: number;
155
165
  }): Promise<void>;
156
166
  listStageEvents(runId: string, stageId: string, attempt?: number): Promise<StageLogEvent[]>;
157
- listRuns(): Promise<RunSummary[]>;
167
+ listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
158
168
  readRun(runId: string): Promise<RunDetail>;
169
+ updatePipelineDag(runId: string, dag: RunPipelineDagSnapshot): Promise<void>;
159
170
  }
160
171
  export declare function stageStatusFromEvents(events: StageLogEvent[]): StageSnapshot["status"];
161
172
  export declare function deriveStatusFromStages(stages: StageSnapshot[]): RunStatus;
@@ -9,6 +9,8 @@ export function stageStatusFromEvents(events) {
9
9
  status = "succeeded";
10
10
  if (ev.event === "failed")
11
11
  status = "failed";
12
+ if (ev.event === "skipped")
13
+ status = "skipped";
12
14
  }
13
15
  return status;
14
16
  }
@@ -17,8 +19,9 @@ export function deriveStatusFromStages(stages) {
17
19
  return "created";
18
20
  if (stages.some((s) => s.status === "failed"))
19
21
  return "failed";
20
- if (stages.every((s) => s.status === "succeeded"))
22
+ if (stages.every((s) => s.status === "succeeded" || s.status === "skipped")) {
21
23
  return "succeeded";
24
+ }
22
25
  if (stages.some((s) => s.status === "running" ||
23
26
  s.status === "waiting_for_input" ||
24
27
  s.status === "succeeded")) {
@@ -1,6 +1,6 @@
1
1
  import type { RunDetail, RunMeta, RunPipelineDagSnapshot, RunSummary, StageSnapshot } from "./port.js";
2
2
  export { syntheticPendingSnapshot } from "./syntheticStageSnapshot.js";
3
- export declare function overlayPlannedStages(stageIds: string[], snapshots: StageSnapshot[]): StageSnapshot[];
3
+ export declare function overlayPlannedStages(stageIds: string[], snapshots: StageSnapshot[], dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null): StageSnapshot[];
4
4
  export declare function orderStageSnapshots(stageIds: string[], snapshots: StageSnapshot[]): StageSnapshot[];
5
5
  export declare function projectRunSummary(meta: RunMeta, stages: StageSnapshot[]): RunSummary;
6
6
  export declare function projectRunDetail(meta: RunMeta, stages: StageSnapshot[], task_yaml: string, dagSnapshot?: RunPipelineDagSnapshot | null): RunDetail;
@@ -3,9 +3,19 @@ import { linearCompatDagSnapshot } from "./pipelineDagSnapshot.js";
3
3
  import { syntheticPendingSnapshot } from "./syntheticStageSnapshot.js";
4
4
  import { buildPipelineTrack } from "./trackProjection.js";
5
5
  export { syntheticPendingSnapshot } from "./syntheticStageSnapshot.js";
6
- export function overlayPlannedStages(stageIds, snapshots) {
6
+ export function overlayPlannedStages(stageIds, snapshots, dag) {
7
7
  const byId = new Map(snapshots.map((s) => [s.stage_id, s]));
8
- return stageIds.map((id) => byId.get(id) ?? syntheticPendingSnapshot(id));
8
+ const nodeById = new Map((dag?.nodes ?? []).map((n) => [n.id, n]));
9
+ return stageIds.map((id) => {
10
+ const node = nodeById.get(id);
11
+ const definitionId = node?.definition_id ?? node?.id;
12
+ const existing = byId.get(id);
13
+ const base = existing ?? syntheticPendingSnapshot(id, definitionId);
14
+ if (definitionId === undefined || base.definition_id === definitionId) {
15
+ return base;
16
+ }
17
+ return { ...base, definition_id: definitionId };
18
+ });
9
19
  }
10
20
  export function orderStageSnapshots(stageIds, snapshots) {
11
21
  return overlayPlannedStages(stageIds, snapshots);
@@ -15,6 +25,7 @@ function compactStages(stages) {
15
25
  id: s.stage_id,
16
26
  status: s.status,
17
27
  attempt_count: s.attempt_count,
28
+ ...(s.definition_id !== undefined ? { definition_id: s.definition_id } : {}),
18
29
  }));
19
30
  }
20
31
  function waitingSummaryFromPrompt(prompt) {
@@ -93,7 +104,7 @@ export function projectRunSummary(meta, stages) {
93
104
  }
94
105
  export function projectRunDetail(meta, stages, task_yaml, dagSnapshot) {
95
106
  const ordered = dagSnapshot
96
- ? overlayPlannedStages(dagSnapshot.stage_ids, stages)
107
+ ? overlayPlannedStages(dagSnapshot.stage_ids, stages, dagSnapshot)
97
108
  : stages;
98
109
  const summary = projectRunSummary(meta, ordered);
99
110
  const snapshot = dagSnapshot ??
@@ -1,6 +1,6 @@
1
1
  import type { StageEnvelope } from "../../types/envelope.js";
2
2
  import type { StageLogLine } from "../../agent/activity.js";
3
- import { type CreateRunInput, type CreatedRun, type RunDetail, type RunMeta, type RunStatus, type RunStore, type RunSummary, type StageExecution, type StageExecutionPatch, type StageLogEvent } from "../port.js";
3
+ import { type CreateRunInput, type CreatedRun, type ListRunsFilter, type RunDetail, type RunMeta, type RunPipelineDagSnapshot, type RunStatus, type RunStore, type RunSummary, type StageExecution, type StageExecutionPatch, type StageLogEvent } from "../port.js";
4
4
  export declare class SqliteRunStore implements RunStore {
5
5
  private readonly storeRoot;
6
6
  private readonly db;
@@ -11,6 +11,7 @@ export declare class SqliteRunStore implements RunStore {
11
11
  getWorkspaceDir(runId: string): string;
12
12
  createRun(input: CreateRunInput): Promise<CreatedRun>;
13
13
  updateRunStatus(runId: string, status: RunStatus): Promise<void>;
14
+ updatePipelineDag(runId: string, dag: RunPipelineDagSnapshot): Promise<void>;
14
15
  readRunMeta(runId: string): Promise<RunMeta>;
15
16
  readTaskYaml(runId: string): Promise<string>;
16
17
  ensureStageWorkspace(runId: string, stageId: string): Promise<void>;
@@ -37,7 +38,7 @@ export declare class SqliteRunStore implements RunStore {
37
38
  */
38
39
  private updateStageStatusDenorm;
39
40
  listStageEvents(runId: string, stageId: string, attempt?: number): Promise<StageLogEvent[]>;
40
- listRuns(): Promise<RunSummary[]>;
41
+ listRuns(filter?: ListRunsFilter): Promise<RunSummary[]>;
41
42
  readRun(runId: string): Promise<RunDetail>;
42
43
  private readPipelineDagSnapshotFromRow;
43
44
  private runMetaFromRow;
@@ -174,6 +174,19 @@ export class SqliteRunStore {
174
174
  throw new Error(`Run not found: ${runId}`);
175
175
  }
176
176
  }
177
+ async updatePipelineDag(runId, dag) {
178
+ await this.ready();
179
+ const result = this.db
180
+ .prepare(`UPDATE runs SET pipeline_dag_json = @pipeline_dag_json, updated_at = @updated_at WHERE run_id = @run_id`)
181
+ .run({
182
+ run_id: runId,
183
+ pipeline_dag_json: JSON.stringify(dag),
184
+ updated_at: new Date().toISOString(),
185
+ });
186
+ if (result.changes === 0) {
187
+ throw new Error(`Run not found: ${runId}`);
188
+ }
189
+ }
177
190
  async readRunMeta(runId) {
178
191
  await this.ready();
179
192
  return this.runMetaFromRow(this.getRunRow(runId));
@@ -411,12 +424,29 @@ export class SqliteRunStore {
411
424
  await this.ready();
412
425
  return this.loadEvents(runId, stageId, attempt);
413
426
  }
414
- async listRuns() {
427
+ async listRuns(filter) {
415
428
  await this.ready();
429
+ const clauses = [];
430
+ const params = [];
431
+ if (filter?.status !== undefined) {
432
+ clauses.push("status = ?");
433
+ params.push(filter.status);
434
+ }
435
+ if (filter?.since !== undefined) {
436
+ clauses.push("created_at >= ?");
437
+ params.push(filter.since);
438
+ }
439
+ if (filter?.pipeline !== undefined && filter.pipeline.trim().length > 0) {
440
+ const pipeline = filter.pipeline.trim();
441
+ const normalized = normalizeCatalogPath(pipeline);
442
+ clauses.push("(pipeline_id = ? OR pipeline_path = ? OR pipeline_path = ?)");
443
+ params.push(pipeline, pipeline, normalized);
444
+ }
445
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
416
446
  const rows = this.db
417
447
  .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root
418
- FROM runs ORDER BY created_at DESC`)
419
- .all();
448
+ FROM runs ${where} ORDER BY created_at DESC`)
449
+ .all(...params);
420
450
  const summaries = [];
421
451
  for (const row of rows) {
422
452
  const dagSnapshot = this.readPipelineDagSnapshotFromRow(row);
@@ -438,6 +468,7 @@ export class SqliteRunStore {
438
468
  return parsePipelineDagSnapshot(JSON.parse(row.pipeline_dag_json));
439
469
  }
440
470
  runMetaFromRow(row) {
471
+ const pipeline_dag = this.readPipelineDagSnapshotFromRow(row);
441
472
  return {
442
473
  run_id: row.run_id,
443
474
  pipeline_id: row.pipeline_id,
@@ -452,6 +483,7 @@ export class SqliteRunStore {
452
483
  ...(row.pipeline_path != null ? { pipeline_path: row.pipeline_path } : {}),
453
484
  ...(row.task_path != null ? { task_path: row.task_path } : {}),
454
485
  ...(row.project_root != null ? { project_root: row.project_root } : {}),
486
+ ...(pipeline_dag ? { pipeline_dag } : {}),
455
487
  };
456
488
  }
457
489
  getRunRow(runId) {
@@ -0,0 +1,4 @@
1
+ import type { RunPipelineDagSnapshot } from "./port.js";
2
+ export declare function mintCloneInstanceId(catalogId: string, n: number): string;
3
+ export declare function mintCloneInstanceIds(catalogId: string, count: number): string[];
4
+ export declare function definitionIdForInstance(snapshot: RunPipelineDagSnapshot | null | undefined, instanceId: string): string;
@@ -0,0 +1,23 @@
1
+ import { guardStageId } from "./workspaceLayout.js";
2
+ function guardPositiveInteger(n, label) {
3
+ if (!Number.isInteger(n) || n < 1) {
4
+ throw new Error(`${label} must be a positive integer`);
5
+ }
6
+ }
7
+ export function mintCloneInstanceId(catalogId, n) {
8
+ guardPositiveInteger(n, "n");
9
+ guardStageId(catalogId);
10
+ const instanceId = `${catalogId}~${n}`;
11
+ guardStageId(instanceId);
12
+ return instanceId;
13
+ }
14
+ export function mintCloneInstanceIds(catalogId, count) {
15
+ guardPositiveInteger(count, "count");
16
+ return Array.from({ length: count }, (_, i) => mintCloneInstanceId(catalogId, i + 1));
17
+ }
18
+ export function definitionIdForInstance(snapshot, instanceId) {
19
+ const node = snapshot?.nodes.find((n) => n.id === instanceId);
20
+ if (!node)
21
+ return instanceId;
22
+ return node.definition_id ?? node.id;
23
+ }
@@ -1,2 +1,2 @@
1
1
  import type { StageSnapshot } from "./port.js";
2
- export declare function syntheticPendingSnapshot(stageId: string): StageSnapshot;
2
+ export declare function syntheticPendingSnapshot(stageId: string, definitionId?: string): StageSnapshot;
@@ -1,4 +1,4 @@
1
- export function syntheticPendingSnapshot(stageId) {
1
+ export function syntheticPendingSnapshot(stageId, definitionId) {
2
2
  return {
3
3
  stage_id: stageId,
4
4
  status: "pending",
@@ -6,5 +6,6 @@ export function syntheticPendingSnapshot(stageId) {
6
6
  envelope: null,
7
7
  artifacts: [],
8
8
  attempt_count: 1,
9
+ ...(definitionId !== undefined ? { definition_id: definitionId } : {}),
9
10
  };
10
11
  }
@@ -1,10 +1,14 @@
1
- import { linearCompatDagSnapshot } from "./pipelineDagSnapshot.js";
1
+ import { instancesOfDefinition, linearCompatDagSnapshot, } from "./pipelineDagSnapshot.js";
2
2
  function stageStarted(stage) {
3
3
  return stage.events.some((e) => e.event === "started" || e.event === "resumed");
4
4
  }
5
5
  function runHalted(stages) {
6
6
  return stages.some((s) => s.status === "failed");
7
7
  }
8
+ function predecessorIds(dag, needs) {
9
+ const preds = instancesOfDefinition(dag, needs);
10
+ return preds.length > 0 ? preds : [needs];
11
+ }
8
12
  function computeLayers(dag) {
9
13
  const layers = new Map();
10
14
  const nodeById = new Map(dag.nodes.map((n) => [n.id, n]));
@@ -17,7 +21,8 @@ function computeLayers(dag) {
17
21
  layers.set(id, 0);
18
22
  return 0;
19
23
  }
20
- const l = layerFor(node.needs) + 1;
24
+ const fromIds = predecessorIds(dag, node.needs);
25
+ const l = Math.max(...fromIds.map(layerFor)) + 1;
21
26
  layers.set(id, l);
22
27
  return l;
23
28
  }
@@ -32,6 +37,8 @@ function deriveReadiness(stage, dag, snapshotsById, halted) {
32
37
  return { readiness: "succeeded" };
33
38
  if (status === "failed")
34
39
  return { readiness: "failed" };
40
+ if (status === "skipped")
41
+ return { readiness: "skipped" };
35
42
  if (status === "running")
36
43
  return { readiness: "running" };
37
44
  if (status === "waiting_for_input")
@@ -42,9 +49,13 @@ function deriveReadiness(stage, dag, snapshotsById, halted) {
42
49
  const node = dag.nodes.find((n) => n.id === stage.stage_id);
43
50
  const need = node?.needs;
44
51
  if (need) {
45
- const predecessor = snapshotsById.get(need);
46
- if (!predecessor || predecessor.status !== "succeeded") {
47
- return { readiness: "blocked", blocked_by: [need] };
52
+ const fromIds = predecessorIds(dag, need);
53
+ const unresolved = fromIds.filter((id) => {
54
+ const predecessor = snapshotsById.get(id);
55
+ return !predecessor || predecessor.status !== "succeeded";
56
+ });
57
+ if (unresolved.length > 0) {
58
+ return { readiness: "blocked", blocked_by: unresolved };
48
59
  }
49
60
  }
50
61
  return { readiness: "ready" };
@@ -63,6 +74,7 @@ export function buildPipelineTrack(input) {
63
74
  }
64
75
  const node = dag.nodes.find((n) => n.id === stageId);
65
76
  const { readiness, blocked_by } = deriveReadiness(stage, dag, snapshotsById, halted);
77
+ const definitionId = node?.definition_id ?? stage.definition_id ?? stageId;
66
78
  const trackNode = {
67
79
  stage_id: stageId,
68
80
  status: stage.status,
@@ -70,10 +82,11 @@ export function buildPipelineTrack(input) {
70
82
  layer: layers.get(stageId) ?? 0,
71
83
  layer_order: node?.stageIndex ?? dag.stage_ids.indexOf(stageId),
72
84
  attempt_count: stage.attempt_count,
85
+ definition_id: definitionId,
73
86
  };
74
87
  if (blocked_by?.length)
75
88
  trackNode.blocked_by = blocked_by;
76
- const gateKinds = dag.gate_kinds?.[stageId];
89
+ const gateKinds = dag.gate_kinds?.[definitionId] ?? dag.gate_kinds?.[stageId];
77
90
  if (gateKinds?.length)
78
91
  trackNode.gate_kinds = gateKinds;
79
92
  return trackNode;
@@ -82,13 +95,16 @@ export function buildPipelineTrack(input) {
82
95
  for (const node of dag.nodes) {
83
96
  if (!node.needs)
84
97
  continue;
85
- const fromStage = snapshotsById.get(node.needs);
86
- const edge = { from: node.needs, to: node.id };
87
- if (fromStage?.status === "succeeded" &&
88
- fromStage.envelope?.summary) {
89
- edge.envelope_summary = fromStage.envelope.summary;
98
+ const fromIds = predecessorIds(dag, node.needs);
99
+ for (const fromId of fromIds) {
100
+ const fromStage = snapshotsById.get(fromId);
101
+ const edge = { from: fromId, to: node.id };
102
+ if (fromStage?.status === "succeeded" &&
103
+ fromStage.envelope?.summary) {
104
+ edge.envelope_summary = fromStage.envelope.summary;
105
+ }
106
+ edges.push(edge);
90
107
  }
91
- edges.push(edge);
92
108
  }
93
109
  return { nodes, edges };
94
110
  }
@@ -1,4 +1,5 @@
1
1
  export { stageDir } from "./paths.js";
2
+ export declare function guardStageId(stageId: string): void;
2
3
  export declare function attemptWorkspaceDir(workspaceDir: string, stageId: string, attempt: number): string;
3
4
  export declare function attemptLogPath(workspaceDir: string, stageId: string, attempt: number): string;
4
5
  export declare function attemptEnvelopePath(workspaceDir: string, stageId: string, attempt: number): string;
@@ -1,7 +1,7 @@
1
1
  import { access, lstat, mkdir, readdir, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  export { stageDir } from "./paths.js";
4
- function guardStageId(stageId) {
4
+ export function guardStageId(stageId) {
5
5
  if (stageId.includes("..") ||
6
6
  stageId.includes("/") ||
7
7
  stageId.includes("\\")) {
@@ -0,0 +1,18 @@
1
+ import type { RunPipelineDagSnapshot } from "../runstore/port.js";
2
+ import type { StageEnvelope } from "../types/envelope.js";
3
+ import type { ResolvedPipelineDag } from "../types/pipeline.js";
4
+ import type { StageScheduleState } from "./pipelineScheduler.js";
5
+ export declare function isCloneInstance(dag: ResolvedPipelineDag, stageId: string): boolean;
6
+ export declare function sequentialLaterCloneIds(dag: ResolvedPipelineDag, stageId: string, completedEnvelopes: Map<string, StageEnvelope>): string[];
7
+ export declare function cloneFanoutConflict(dag: ResolvedPipelineDag, envelope: StageEnvelope): string | undefined;
8
+ export declare function protectedClonableChildIds(dag: ResolvedPipelineDag, predecessorId: string, envelope: StageEnvelope): Set<string>;
9
+ export declare function cloneRetryDownstream(dag: ResolvedPipelineDag, stageIds: string[]): Set<string>;
10
+ export declare function cloneFailureContinuesSchedule(dag: ResolvedPipelineDag, stageId: string, completedEnvelopes: Map<string, StageEnvelope>): boolean;
11
+ export declare function cloneFailFastSkipIds(dag: ResolvedPipelineDag, stageId: string, completedEnvelopes: Map<string, StageEnvelope>): string[];
12
+ export declare function cloneScheduleAllowsRun(dag: ResolvedPipelineDag, stageId: string, states: Map<string, StageScheduleState>, completedEnvelopes: Map<string, StageEnvelope>): boolean;
13
+ export type ApplyCloneForksResult = {
14
+ dag: RunPipelineDagSnapshot;
15
+ skippedIds: string[];
16
+ };
17
+ export declare function applyCloneForksToSchedule(dag: ResolvedPipelineDag, predecessorId: string, envelope: StageEnvelope, states: Map<string, StageScheduleState>): ApplyCloneForksResult;
18
+ export declare function applyCloneForksFromEnvelopes(dag: ResolvedPipelineDag, states: Map<string, StageScheduleState>, completedEnvelopes: Map<string, StageEnvelope>): ApplyCloneForksResult;