stageflow 0.2.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +179 -14
  2. package/dist/agent/activity.d.ts +12 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/cursorProvider.d.ts +1 -10
  7. package/dist/agent/cursorProvider.js +18 -10
  8. package/dist/agent/fakeAgent.js +15 -7
  9. package/dist/agent/piAdapter.d.ts +18 -1
  10. package/dist/agent/piAdapter.js +115 -45
  11. package/dist/agent/port.d.ts +7 -0
  12. package/dist/agent/port.js +3 -0
  13. package/dist/agent/providerAuth.js +12 -7
  14. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  15. package/dist/catalog/displayCatalogPath.js +28 -0
  16. package/dist/cli/artifactCommand.d.ts +10 -0
  17. package/dist/cli/artifactCommand.js +120 -0
  18. package/dist/cli/envelopeCommand.d.ts +32 -0
  19. package/dist/cli/envelopeCommand.js +285 -0
  20. package/dist/cli/exportRunCommand.d.ts +10 -0
  21. package/dist/cli/exportRunCommand.js +150 -0
  22. package/dist/cli/handoffFormat.d.ts +22 -0
  23. package/dist/cli/handoffFormat.js +61 -0
  24. package/dist/cli/initCommand.d.ts +9 -0
  25. package/dist/cli/initCommand.js +71 -0
  26. package/dist/cli/initTemplates.d.ts +3 -0
  27. package/dist/cli/initTemplates.js +19 -0
  28. package/dist/cli/operatorCatalog.d.ts +10 -0
  29. package/dist/cli/operatorCatalog.js +16 -0
  30. package/dist/cli/runCommand.d.ts +7 -1
  31. package/dist/cli/runCommand.js +83 -11
  32. package/dist/cli/runOutput.d.ts +4 -1
  33. package/dist/cli/runOutput.js +36 -8
  34. package/dist/cli/skillsCommand.d.ts +16 -0
  35. package/dist/cli/skillsCommand.js +459 -0
  36. package/dist/cli/validateCommand.d.ts +1 -1
  37. package/dist/cli/validateCommand.js +20 -3
  38. package/dist/cli.d.ts +1 -0
  39. package/dist/cli.js +128 -22
  40. package/dist/config/browseCatalog.d.ts +45 -0
  41. package/dist/config/browseCatalog.js +170 -0
  42. package/dist/config/createPipeline.d.ts +12 -3
  43. package/dist/config/createPipeline.js +292 -100
  44. package/dist/config/createStage.d.ts +10 -4
  45. package/dist/config/createStage.js +44 -14
  46. package/dist/config/listConfig.d.ts +14 -19
  47. package/dist/config/listConfig.js +26 -191
  48. package/dist/config/listModelsFromManifest.d.ts +2 -0
  49. package/dist/config/listModelsFromManifest.js +4 -0
  50. package/dist/config/loadOutcome.d.ts +4 -0
  51. package/dist/config/loadPipeline.d.ts +1 -15
  52. package/dist/config/loadPipeline.js +88 -142
  53. package/dist/config/loadStage.d.ts +4 -0
  54. package/dist/config/loadStage.js +60 -32
  55. package/dist/config/loadStageflowManifest.d.ts +5 -0
  56. package/dist/config/loadStageflowManifest.js +157 -0
  57. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  58. package/dist/config/mergePipelineIncludes.js +141 -0
  59. package/dist/config/normalizePipelineStageEntry.d.ts +18 -0
  60. package/dist/config/normalizePipelineStageEntry.js +210 -0
  61. package/dist/config/pipelineStageKeys.d.ts +4 -0
  62. package/dist/config/pipelineStageKeys.js +14 -0
  63. package/dist/config/resolveCatalogContext.d.ts +13 -0
  64. package/dist/config/resolveCatalogContext.js +13 -0
  65. package/dist/config/resolveForkEmitContext.d.ts +4 -0
  66. package/dist/config/resolveForkEmitContext.js +34 -0
  67. package/dist/config/resolvePipelineDag.d.ts +5 -1
  68. package/dist/config/resolvePipelineDag.js +86 -36
  69. package/dist/config/scanCatalogPaths.d.ts +4 -0
  70. package/dist/config/scanCatalogPaths.js +83 -0
  71. package/dist/config/validateCatalog.d.ts +22 -4
  72. package/dist/config/validateCatalog.js +169 -132
  73. package/dist/envelope/check.d.ts +2 -0
  74. package/dist/envelope/check.js +58 -0
  75. package/dist/envelope/cloneForks.d.ts +2 -0
  76. package/dist/envelope/cloneForks.js +62 -0
  77. package/dist/envelope/forkChoice.d.ts +3 -0
  78. package/dist/envelope/forkChoice.js +47 -0
  79. package/dist/index.d.ts +6 -0
  80. package/dist/index.js +6 -0
  81. package/dist/mcp/catalogTools.d.ts +3 -0
  82. package/dist/mcp/catalogTools.js +187 -0
  83. package/dist/mcp/controlTools.d.ts +3 -0
  84. package/dist/mcp/controlTools.js +147 -0
  85. package/dist/mcp/deps.d.ts +11 -0
  86. package/dist/mcp/deps.js +1 -0
  87. package/dist/mcp/projectRun.d.ts +1 -37
  88. package/dist/mcp/projectRun.js +1 -57
  89. package/dist/mcp/resources.d.ts +5 -0
  90. package/dist/mcp/resources.js +45 -0
  91. package/dist/mcp/server.d.ts +16 -3
  92. package/dist/mcp/server.js +159 -3
  93. package/dist/mcp/toolResults.d.ts +7 -0
  94. package/dist/mcp/toolResults.js +6 -0
  95. package/dist/mcp/tools.d.ts +2 -7
  96. package/dist/mcp/tools.js +52 -86
  97. package/dist/mcp/waitRun.d.ts +46 -0
  98. package/dist/mcp/waitRun.js +146 -0
  99. package/dist/mcp/waitingGates.d.ts +12 -0
  100. package/dist/mcp/waitingGates.js +83 -0
  101. package/dist/project/findProjectRoot.d.ts +2 -0
  102. package/dist/project/findProjectRoot.js +51 -0
  103. package/dist/project/globalHome.d.ts +2 -0
  104. package/dist/project/globalHome.js +19 -0
  105. package/dist/project/resolveProjectContext.d.ts +9 -0
  106. package/dist/project/resolveProjectContext.js +38 -0
  107. package/dist/project/resolveStageflowContext.d.ts +11 -0
  108. package/dist/project/resolveStageflowContext.js +66 -0
  109. package/dist/projection/projectRun.d.ts +44 -0
  110. package/dist/projection/projectRun.js +71 -0
  111. package/dist/prompt/priorEnvelope.d.ts +1 -1
  112. package/dist/prompt/priorEnvelope.js +4 -1
  113. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  114. package/dist/runstore/normalizeCatalogPath.js +4 -0
  115. package/dist/runstore/paths.js +2 -0
  116. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  117. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  118. package/dist/runstore/port.d.ts +22 -2
  119. package/dist/runstore/port.js +4 -1
  120. package/dist/runstore/runProjection.d.ts +1 -1
  121. package/dist/runstore/runProjection.js +25 -4
  122. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  123. package/dist/runstore/sqlite/SqliteRunStore.js +65 -7
  124. package/dist/runstore/sqlite/schema.d.ts +1 -1
  125. package/dist/runstore/sqlite/schema.js +4 -1
  126. package/dist/runstore/stageInstanceId.d.ts +4 -0
  127. package/dist/runstore/stageInstanceId.js +23 -0
  128. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  129. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  130. package/dist/runstore/trackProjection.js +28 -12
  131. package/dist/runstore/workspaceLayout.d.ts +1 -0
  132. package/dist/runstore/workspaceLayout.js +1 -1
  133. package/dist/runtime/cloneSchedule.d.ts +18 -0
  134. package/dist/runtime/cloneSchedule.js +233 -0
  135. package/dist/runtime/credentialBinding.d.ts +8 -6
  136. package/dist/runtime/credentialBinding.js +18 -24
  137. package/dist/runtime/envelopeRouting.d.ts +2 -1
  138. package/dist/runtime/envelopeRouting.js +80 -13
  139. package/dist/runtime/pipelineRunner.d.ts +3 -0
  140. package/dist/runtime/pipelineRunner.js +15 -1
  141. package/dist/runtime/pipelineScheduler.d.ts +8 -2
  142. package/dist/runtime/pipelineScheduler.js +215 -39
  143. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  144. package/dist/runtime/reloadRunCatalog.js +44 -0
  145. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  146. package/dist/runtime/resumeReconstruct.js +31 -10
  147. package/dist/runtime/runChangeBus.d.ts +18 -0
  148. package/dist/runtime/runChangeBus.js +83 -0
  149. package/dist/runtime/runManager.d.ts +4 -0
  150. package/dist/runtime/runManager.js +36 -16
  151. package/dist/runtime/settingsFile.d.ts +12 -0
  152. package/dist/runtime/settingsFile.js +87 -6
  153. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  154. package/dist/runtime/stageAttemptBootstrap.js +18 -7
  155. package/dist/runtime/stageProcessLauncher.js +3 -0
  156. package/dist/runtime/stageRecovery.js +3 -1
  157. package/dist/runtime/stageRunner.d.ts +1 -0
  158. package/dist/runtime/stageRunner.js +17 -15
  159. package/dist/runtime/stageWorker.js +13 -10
  160. package/dist/server/bootstrap.d.ts +32 -0
  161. package/dist/server/bootstrap.js +56 -0
  162. package/dist/server/createHttpHost.d.ts +34 -0
  163. package/dist/server/createHttpHost.js +70 -0
  164. package/dist/server/http.d.ts +6 -13
  165. package/dist/server/http.js +297 -352
  166. package/dist/server/mcpHost.d.ts +22 -0
  167. package/dist/server/mcpHost.js +21 -0
  168. package/dist/server/operatorResults.d.ts +13 -0
  169. package/dist/server/operatorResults.js +22 -0
  170. package/dist/tools/emitStageEnvelope.d.ts +12 -1
  171. package/dist/tools/emitStageEnvelope.js +27 -1
  172. package/dist/types/envelope.d.ts +3 -0
  173. package/dist/types/forkChoice.d.ts +30 -0
  174. package/dist/types/forkChoice.js +1 -0
  175. package/dist/types/pipeline.d.ts +59 -2
  176. package/dist/types/stageflowManifest.d.ts +23 -0
  177. package/dist/types/stageflowManifest.js +1 -0
  178. package/dist/ui/assets/{index-DCsDopak.css → index-C3N7MXAC.css} +1 -1
  179. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  180. package/dist/ui/index.html +3 -2
  181. package/dist/ui/stageflow-icon.svg +12 -0
  182. package/package.json +8 -5
  183. package/skills/install-suite.sh +107 -0
  184. package/skills/stageflow/SKILL.md +26 -0
  185. package/skills/stageflow/references/control-surface.md +26 -0
  186. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  187. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  188. package/skills/stageflow-author/SKILL.md +58 -0
  189. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  190. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  191. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  192. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  193. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  194. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  195. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  196. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  197. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  198. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  199. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  200. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  201. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  202. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  203. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  204. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  205. package/skills/stageflow-delegate/SKILL.md +18 -0
  206. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  207. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  208. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  209. package/skills/stageflow-run/SKILL.md +151 -0
  210. package/skills/stageflow-run/references/mcp-call.md +17 -0
  211. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  212. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  213. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  214. package/skills/stageflow-session-capture/SKILL.md +94 -0
  215. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  216. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  217. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  218. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  219. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  220. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  221. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  222. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  223. package/skills/stageflow-setup/SKILL.md +110 -0
  224. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -0,0 +1,83 @@
1
+ export class RunChangeBus {
2
+ listeners = new Set();
3
+ on(listener) {
4
+ this.listeners.add(listener);
5
+ return () => {
6
+ this.listeners.delete(listener);
7
+ };
8
+ }
9
+ off(listener) {
10
+ this.listeners.delete(listener);
11
+ }
12
+ emit(event) {
13
+ for (const listener of [...this.listeners]) {
14
+ try {
15
+ listener(event);
16
+ }
17
+ catch {
18
+ // Listener isolation: one throw must not block others.
19
+ }
20
+ }
21
+ }
22
+ }
23
+ export function createRunChangeBus() {
24
+ return new RunChangeBus();
25
+ }
26
+ const WRAPPED_MARKER = Symbol.for("stageflow.wrapRunStoreWithChangeBus");
27
+ const BUS_MARKER = Symbol.for("stageflow.runChangeBus");
28
+ export function isRunStoreWrapped(store) {
29
+ return store[WRAPPED_MARKER] === true;
30
+ }
31
+ export function getRunChangeBusFromWrappedStore(store) {
32
+ const bus = store[BUS_MARKER];
33
+ return bus instanceof RunChangeBus ? bus : undefined;
34
+ }
35
+ export function wrapRunStoreWithChangeBus(store, bus) {
36
+ if (isRunStoreWrapped(store)) {
37
+ const bound = getRunChangeBusFromWrappedStore(store);
38
+ if (bound !== bus) {
39
+ throw new Error("runChangeBus does not match the bus already bound to the provided store");
40
+ }
41
+ return store;
42
+ }
43
+ const createRun = async (input) => {
44
+ const created = await store.createRun(input);
45
+ bus.emit({ runId: created.runId, kind: "created" });
46
+ return created;
47
+ };
48
+ const updateRunStatus = async (runId, status) => {
49
+ await store.updateRunStatus(runId, status);
50
+ bus.emit({ runId, kind: "status" });
51
+ };
52
+ const appendStageEvent = async (runId, stageId, event, options) => {
53
+ await store.appendStageEvent(runId, stageId, event, options);
54
+ if (event.event === "waiting_for_input" || event.event === "resumed") {
55
+ bus.emit({ runId, kind: "waiting" });
56
+ }
57
+ };
58
+ const boundCache = new Map();
59
+ return new Proxy(store, {
60
+ get(target, prop, receiver) {
61
+ if (prop === WRAPPED_MARKER)
62
+ return true;
63
+ if (prop === BUS_MARKER)
64
+ return bus;
65
+ if (prop === "createRun")
66
+ return createRun;
67
+ if (prop === "updateRunStatus")
68
+ return updateRunStatus;
69
+ if (prop === "appendStageEvent")
70
+ return appendStageEvent;
71
+ const cached = boundCache.get(prop);
72
+ if (cached !== undefined)
73
+ return cached;
74
+ const value = Reflect.get(target, prop, receiver);
75
+ if (typeof value === "function") {
76
+ const bound = value.bind(target);
77
+ boundCache.set(prop, bound);
78
+ return bound;
79
+ }
80
+ return value;
81
+ },
82
+ });
83
+ }
@@ -61,10 +61,14 @@ export declare class RunManager {
61
61
  private readonly retryCoordinator;
62
62
  private readonly retryTracking;
63
63
  private readonly cwd;
64
+ private readonly projectRoot;
65
+ private readonly isGitProject;
64
66
  constructor(options: {
65
67
  agent: AgentPort;
66
68
  store: RunStore;
67
69
  cwd?: string;
70
+ projectRoot?: string;
71
+ isGitProject?: boolean;
68
72
  operatorCatalog?: OperatorCatalog;
69
73
  seams?: HitlSeams;
70
74
  maxConcurrent?: number;
@@ -1,14 +1,15 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFile, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { loadPipeline } from "../config/loadPipeline.js";
4
+ import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
5
+ import { loadRunContext } from "./resumeReconstruct.js";
5
6
  import { loadTaskFromYaml } from "../config/loadTask.js";
6
7
  import { PipelineValidationError, startPipeline, } from "./pipelineRunner.js";
7
8
  import { resumeRun } from "./pipelineScheduler.js";
8
9
  import { RunRetryCoordinator, } from "./runRetryCoordinator.js";
9
10
  import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
10
11
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
11
- import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromFile, writeMaxConcurrentToFile, } from "./settingsFile.js";
12
+ import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, projectSettingsContext, readMaxConcurrentFromContext, writeMaxConcurrentToContext, } from "./settingsFile.js";
12
13
  import { StageHitlController, waitKey, } from "./stageHitl.js";
13
14
  import { resolveAndValidateCheckout, } from "./stageRoots.js";
14
15
  import { orchestrateAnswerResume } from "./answerResume.js";
@@ -79,12 +80,17 @@ export class RunManager {
79
80
  },
80
81
  };
81
82
  cwd;
83
+ projectRoot;
84
+ isGitProject;
82
85
  constructor(options) {
83
86
  this.options = options;
84
87
  this.cwd = options.cwd ?? process.cwd();
88
+ this.projectRoot = options.projectRoot ?? this.cwd;
89
+ this.isGitProject = options.isGitProject ?? false;
90
+ const settingsCtx = projectSettingsContext(this.cwd, this.projectRoot, this.isGitProject);
85
91
  this.maxConcurrent =
86
92
  options.maxConcurrent ??
87
- readMaxConcurrentFromFile(this.cwd) ??
93
+ readMaxConcurrentFromContext(settingsCtx) ??
88
94
  parseMaxConcurrent(process.env.STAGEFLOW_MAX_CONCURRENT_RUNS);
89
95
  this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
90
96
  this.executionMode = readStageExecutionMode(process.env, options.executionMode);
@@ -106,7 +112,7 @@ export class RunManager {
106
112
  throw new Error(INVALID_SLOT_COUNT_MESSAGE);
107
113
  }
108
114
  this.maxConcurrent = parsed;
109
- writeMaxConcurrentToFile(this.cwd, parsed);
115
+ writeMaxConcurrentToContext(projectSettingsContext(this.cwd, this.projectRoot, this.isGitProject), parsed);
110
116
  return this.getHealth();
111
117
  }
112
118
  getActiveCount() {
@@ -355,17 +361,32 @@ export class RunManager {
355
361
  }
356
362
  async rerun(runId) {
357
363
  const cwd = this.options.cwd ?? process.cwd();
358
- let pipelineId;
364
+ let pipeline;
359
365
  let taskYaml;
366
+ let rerunCwd = cwd;
367
+ let rerunProjectRoot;
360
368
  try {
361
369
  const meta = await this.options.store.readRunMeta(runId);
362
- pipelineId = meta.pipeline_id;
370
+ if (!meta.pipeline_path) {
371
+ return {
372
+ ok: false,
373
+ reason: `Run ${runId} is missing pipeline_path; re-run requires stored catalog locators.`,
374
+ status: 400,
375
+ };
376
+ }
377
+ pipeline = normalizeCatalogPath(meta.pipeline_path);
378
+ rerunCwd = meta.project_root
379
+ ? normalizeCatalogPath(meta.project_root)
380
+ : cwd;
381
+ rerunProjectRoot = meta.project_root
382
+ ? normalizeCatalogPath(meta.project_root)
383
+ : undefined;
363
384
  taskYaml = await this.options.store.readTaskYaml(runId);
364
385
  }
365
386
  catch {
366
387
  return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
367
388
  }
368
- return this.reserveAndStartPipeline(taskYaml, pipelineId, `run ${runId} task`, cwd);
389
+ return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, undefined, undefined, rerunProjectRoot);
369
390
  }
370
391
  async retryStage(runId, stageId) {
371
392
  const retryKey = waitKey(runId, stageId);
@@ -489,7 +510,6 @@ export class RunManager {
489
510
  }
490
511
  }
491
512
  async resumeViaSubprocess(runId, stageId, opaqueAnswer) {
492
- const cwd = this.options.cwd ?? process.cwd();
493
513
  const store = this.options.store;
494
514
  const launcher = this.stageProcessLauncher;
495
515
  if (!launcher) {
@@ -504,7 +524,7 @@ export class RunManager {
504
524
  const launchResult = await launcher.launch({
505
525
  runId,
506
526
  stageId,
507
- rootDir: cwd,
527
+ rootDir: this.projectRoot,
508
528
  mode: "resume",
509
529
  resumeAnswer: opaqueAnswer,
510
530
  attempt,
@@ -531,10 +551,7 @@ export class RunManager {
531
551
  catch {
532
552
  // downstream resume may read envelope from store
533
553
  }
534
- const meta = await store.readRunMeta(runId);
535
- const taskYaml = await store.readTaskYaml(runId);
536
- const task = loadTaskFromYaml(taskYaml, `run ${runId} task`);
537
- const loaded = await loadPipeline(meta.pipeline_id, { cwd });
554
+ const { meta, task, loaded } = await loadRunContext(store, runId, this.cwd);
538
555
  const rest = await resumeRun({
539
556
  prepared: {
540
557
  task,
@@ -542,7 +559,8 @@ export class RunManager {
542
559
  run: { runId, workspaceDir: store.getWorkspaceDir(runId) },
543
560
  agent: this.options.agent,
544
561
  store,
545
- cwd,
562
+ cwd: this.cwd,
563
+ projectRoot: this.projectRoot,
546
564
  checkoutRoot: meta.checkout_root,
547
565
  hitl: this.hitl,
548
566
  operatorCatalog: this.options.operatorCatalog,
@@ -590,6 +608,7 @@ export class RunManager {
590
608
  executionMode: this.executionMode,
591
609
  stageProcessLauncher: this.stageProcessLauncher,
592
610
  cwd: this.cwd,
611
+ factoryCwd: this.projectRoot,
593
612
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
594
613
  operatorCatalog: this.options.operatorCatalog,
595
614
  });
@@ -598,7 +617,7 @@ export class RunManager {
598
617
  this.attachedWaiting.delete(`${runId}\0${stageId}`);
599
618
  }
600
619
  }
601
- async reserveAndStartPipeline(taskYaml, pipelineId, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity) {
620
+ async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot) {
602
621
  let checkoutKey;
603
622
  try {
604
623
  const task = loadTaskFromYaml(taskYaml, taskLabel);
@@ -623,8 +642,9 @@ export class RunManager {
623
642
  agent: this.options.agent,
624
643
  store: this.options.store,
625
644
  taskYaml,
626
- pipeline: pipelineId,
645
+ pipeline,
627
646
  cwd,
647
+ projectRoot: projectRoot ?? this.projectRoot,
628
648
  checkoutOverride,
629
649
  gitSha: ciIdentity?.gitSha,
630
650
  ciPrUrl: ciIdentity?.ciPrUrl,
@@ -1,3 +1,5 @@
1
+ import type { ProjectContext } from "../project/resolveProjectContext.js";
2
+ import type { StageflowContext } from "../project/resolveStageflowContext.js";
1
3
  export declare const SETTINGS_FILE_NAME = "settings.json";
2
4
  export declare const INVALID_SLOT_COUNT_MESSAGE = "maxConcurrent must be an integer >= 1";
3
5
  export declare const INVALID_CREDENTIAL_SOURCE_MESSAGE = "credentialSource must be \"pi_home\" or \"sf_owned\"";
@@ -6,12 +8,22 @@ export type FactorySettings = {
6
8
  maxConcurrent?: number;
7
9
  credentialSource?: CredentialSource;
8
10
  };
11
+ export declare function globalSettingsFilePath(): string;
12
+ export declare function settingsFilePathForContext(ctx: ProjectContext | StageflowContext): string;
9
13
  export declare function settingsFilePath(cwd: string): string;
10
14
  export declare function parseSlotCount(value: unknown): number | undefined;
11
15
  export declare function parseCredentialSource(value: unknown): CredentialSource | undefined;
16
+ export declare function readCredentialSourceFromContext(ctx: ProjectContext): CredentialSource | undefined;
17
+ export declare function readPersistedCredentialSourceFromContext(ctx: ProjectContext): CredentialSource | undefined;
18
+ export declare function readFactorySettingsForContext(ctx: ProjectContext): FactorySettings;
12
19
  export declare function readFactorySettings(cwd: string): FactorySettings;
20
+ export declare function writeFactorySettingsForContext(ctx: ProjectContext, patch: FactorySettings): void;
13
21
  export declare function writeFactorySettings(cwd: string, patch: FactorySettings): void;
22
+ export declare function readMaxConcurrentFromContext(ctx: ProjectContext): number | undefined;
14
23
  export declare function readMaxConcurrentFromFile(cwd: string): number | undefined;
24
+ export declare function writeMaxConcurrentToContext(ctx: ProjectContext, maxConcurrent: number): void;
15
25
  export declare function writeMaxConcurrentToFile(cwd: string, maxConcurrent: number): void;
16
26
  export declare function readCredentialSourceFromFile(cwd: string): CredentialSource | undefined;
27
+ export declare function writeCredentialSourceToContext(ctx: ProjectContext, credentialSource: CredentialSource): void;
17
28
  export declare function writeCredentialSourceToFile(cwd: string, credentialSource: CredentialSource): void;
29
+ export declare function projectSettingsContext(cwd: string, projectRoot: string, isGitProject: boolean): ProjectContext;
@@ -1,11 +1,25 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
4
+ import { globalStageflowHome } from "../project/globalHome.js";
3
5
  import { storeRootFor } from "../runstore/paths.js";
4
6
  export const SETTINGS_FILE_NAME = "settings.json";
5
7
  export const INVALID_SLOT_COUNT_MESSAGE = "maxConcurrent must be an integer >= 1";
6
8
  export const INVALID_CREDENTIAL_SOURCE_MESSAGE = 'credentialSource must be "pi_home" or "sf_owned"';
9
+ export function globalSettingsFilePath() {
10
+ return path.join(globalStageflowHome(), SETTINGS_FILE_NAME);
11
+ }
12
+ export function settingsFilePathForContext(ctx) {
13
+ if (ctx.isGitProject) {
14
+ return path.join(storeRootFor(ctx.projectRoot), SETTINGS_FILE_NAME);
15
+ }
16
+ if (path.resolve(ctx.projectRoot) === path.resolve(os.homedir())) {
17
+ return path.join(ctx.globalHome, SETTINGS_FILE_NAME);
18
+ }
19
+ return path.join(storeRootFor(ctx.projectRoot), SETTINGS_FILE_NAME);
20
+ }
7
21
  export function settingsFilePath(cwd) {
8
- return path.join(storeRootFor(cwd), SETTINGS_FILE_NAME);
22
+ return path.join(storeRootFor(path.resolve(cwd)), SETTINGS_FILE_NAME);
9
23
  }
10
24
  export function parseSlotCount(value) {
11
25
  if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
@@ -19,8 +33,7 @@ export function parseCredentialSource(value) {
19
33
  }
20
34
  return undefined;
21
35
  }
22
- function readRawSettings(cwd) {
23
- const filePath = settingsFilePath(cwd);
36
+ function readRawSettingsFromFile(filePath) {
24
37
  if (!existsSync(filePath))
25
38
  return {};
26
39
  try {
@@ -34,8 +47,10 @@ function readRawSettings(cwd) {
34
47
  return {};
35
48
  }
36
49
  }
37
- export function readFactorySettings(cwd) {
38
- const raw = readRawSettings(cwd);
50
+ function readRawSettings(ctx) {
51
+ return readRawSettingsFromFile(settingsFilePathForContext(ctx));
52
+ }
53
+ function parseFactorySettingsFromRaw(raw) {
39
54
  const settings = {};
40
55
  const maxConcurrent = parseSlotCount(raw.maxConcurrent);
41
56
  if (maxConcurrent !== undefined) {
@@ -47,6 +62,54 @@ export function readFactorySettings(cwd) {
47
62
  }
48
63
  return settings;
49
64
  }
65
+ export function readCredentialSourceFromContext(ctx) {
66
+ if (ctx.isGitProject) {
67
+ const fromProject = parseCredentialSource(readRawSettingsFromFile(settingsFilePathForContext(ctx)).credentialSource);
68
+ if (fromProject !== undefined) {
69
+ return fromProject;
70
+ }
71
+ }
72
+ return parseCredentialSource(readRawSettingsFromFile(globalSettingsFilePath()).credentialSource);
73
+ }
74
+ export function readPersistedCredentialSourceFromContext(ctx) {
75
+ return parseCredentialSource(readRawSettingsFromFile(settingsFilePathForContext(ctx)).credentialSource);
76
+ }
77
+ export function readFactorySettingsForContext(ctx) {
78
+ return parseFactorySettingsFromRaw(readRawSettings(ctx));
79
+ }
80
+ export function readFactorySettings(cwd) {
81
+ return parseFactorySettingsFromRaw(readRawSettingsFromFile(settingsFilePath(cwd)));
82
+ }
83
+ function ensureSettingsDirForContext(ctx) {
84
+ mkdirSync(path.dirname(settingsFilePathForContext(ctx)), { recursive: true });
85
+ }
86
+ export function writeFactorySettingsForContext(ctx, patch) {
87
+ if (patch.credentialSource !== undefined &&
88
+ parseCredentialSource(patch.credentialSource) === undefined) {
89
+ throw new Error(INVALID_CREDENTIAL_SOURCE_MESSAGE);
90
+ }
91
+ if (patch.maxConcurrent !== undefined &&
92
+ parseSlotCount(patch.maxConcurrent) === undefined) {
93
+ throw new Error(INVALID_SLOT_COUNT_MESSAGE);
94
+ }
95
+ const current = readFactorySettingsForContext(ctx);
96
+ const next = { ...current };
97
+ if (patch.maxConcurrent !== undefined) {
98
+ next.maxConcurrent = patch.maxConcurrent;
99
+ }
100
+ if (patch.credentialSource !== undefined) {
101
+ next.credentialSource = patch.credentialSource;
102
+ }
103
+ const out = {};
104
+ if (next.maxConcurrent !== undefined) {
105
+ out.maxConcurrent = next.maxConcurrent;
106
+ }
107
+ if (next.credentialSource !== undefined) {
108
+ out.credentialSource = next.credentialSource;
109
+ }
110
+ ensureSettingsDirForContext(ctx);
111
+ writeFileSync(settingsFilePathForContext(ctx), `${JSON.stringify(out, null, 2)}\n`, "utf8");
112
+ }
50
113
  export function writeFactorySettings(cwd, patch) {
51
114
  if (patch.credentialSource !== undefined &&
52
115
  parseCredentialSource(patch.credentialSource) === undefined) {
@@ -71,18 +134,36 @@ export function writeFactorySettings(cwd, patch) {
71
134
  if (next.credentialSource !== undefined) {
72
135
  out.credentialSource = next.credentialSource;
73
136
  }
74
- mkdirSync(storeRootFor(cwd), { recursive: true });
137
+ const root = path.resolve(cwd);
138
+ mkdirSync(storeRootFor(root), { recursive: true });
75
139
  writeFileSync(settingsFilePath(cwd), `${JSON.stringify(out, null, 2)}\n`, "utf8");
76
140
  }
141
+ export function readMaxConcurrentFromContext(ctx) {
142
+ return readFactorySettingsForContext(ctx).maxConcurrent;
143
+ }
77
144
  export function readMaxConcurrentFromFile(cwd) {
78
145
  return readFactorySettings(cwd).maxConcurrent;
79
146
  }
147
+ export function writeMaxConcurrentToContext(ctx, maxConcurrent) {
148
+ writeFactorySettingsForContext(ctx, { maxConcurrent });
149
+ }
80
150
  export function writeMaxConcurrentToFile(cwd, maxConcurrent) {
81
151
  writeFactorySettings(cwd, { maxConcurrent });
82
152
  }
83
153
  export function readCredentialSourceFromFile(cwd) {
84
154
  return readFactorySettings(cwd).credentialSource;
85
155
  }
156
+ export function writeCredentialSourceToContext(ctx, credentialSource) {
157
+ writeFactorySettingsForContext(ctx, { credentialSource });
158
+ }
86
159
  export function writeCredentialSourceToFile(cwd, credentialSource) {
87
160
  writeFactorySettings(cwd, { credentialSource });
88
161
  }
162
+ export function projectSettingsContext(cwd, projectRoot, isGitProject) {
163
+ return {
164
+ invocationCwd: path.resolve(cwd),
165
+ projectRoot: path.resolve(projectRoot),
166
+ globalHome: globalStageflowHome(),
167
+ isGitProject,
168
+ };
169
+ }
@@ -27,6 +27,7 @@ export type StageAttemptOpenInput = {
27
27
  resumeToken?: string;
28
28
  workerRoots?: boolean;
29
29
  completedEnvelopes?: Map<string, StageEnvelope>;
30
+ stageId?: string;
30
31
  };
31
32
  export type StageAttemptOpenResult = {
32
33
  ok: true;
@@ -1,4 +1,5 @@
1
1
  import { resolveSkillByName } from "../config/listSkills.js";
2
+ import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
2
3
  import { buildCompletedEnvelopesFromRun, resolvePriorEnvelope, } from "./envelopeRouting.js";
3
4
  import { resumeSessionFilePath } from "./stageAttemptContext.js";
4
5
  import { buildStageRoots, rootsForStageWorker, withResolvedAuthPath, } from "./stageRoots.js";
@@ -38,38 +39,48 @@ async function openStageWithOperatorCatalog(agent, input, catalog) {
38
39
  };
39
40
  }
40
41
  }
41
- function resolveAttemptRoots(input) {
42
+ function resolveAttemptRoots(input, stageId) {
42
43
  const baseRoots = input.roots ??
43
44
  (input.workerRoots
44
- ? rootsForStageWorker(input.workspaceDir, input.stage.id, input.stage.model, input.checkoutRoot, input.attemptCtx)
45
- : buildStageRoots(input.workspaceDir, input.stage.id, input.checkoutRoot, input.attemptCtx));
45
+ ? rootsForStageWorker(input.workspaceDir, stageId, input.stage.model, input.checkoutRoot, input.attemptCtx)
46
+ : buildStageRoots(input.workspaceDir, stageId, input.checkoutRoot, input.attemptCtx));
46
47
  return input.factoryCwd !== undefined
47
48
  ? withResolvedAuthPath(baseRoots, input.factoryCwd)
48
49
  : baseRoots;
49
50
  }
50
51
  export async function openStageAttempt(input) {
52
+ const stageId = input.stageId ?? input.stage.id;
53
+ const definitionId = input.stage.id;
51
54
  const completedEnvelopes = input.completedEnvelopes ??
52
- (await buildCompletedEnvelopesFromRun(input.store, input.runId));
55
+ (await buildCompletedEnvelopesFromRun(input.store, input.runId, undefined, input.dag));
53
56
  const priorResult = await resolvePriorEnvelope({
54
57
  dag: input.dag,
55
- stageId: input.stage.id,
58
+ stageId,
56
59
  completedEnvelopes,
57
60
  store: input.store,
58
61
  runId: input.runId,
59
62
  });
60
63
  if (!priorResult.ok)
61
64
  return priorResult;
62
- const roots = resolveAttemptRoots(input);
65
+ const roots = resolveAttemptRoots(input, stageId);
63
66
  const attempt = input.attemptCtx?.attempt ?? 1;
64
67
  const resumeToken = input.resumeToken ??
65
- resumeSessionFilePath(input.workspaceDir, input.stage.id, attempt);
68
+ resumeSessionFilePath(input.workspaceDir, stageId, attempt);
69
+ const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
70
+ const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId);
66
71
  const opened = await openStageWithOperatorCatalog(input.agent, {
67
72
  roots,
68
73
  stage: input.stage,
74
+ stageId,
69
75
  task: input.task,
70
76
  priorEnvelope: priorResult.prior,
77
+ ...(priorResult.joinPriors !== undefined
78
+ ? { priorEnvelopes: priorResult.joinPriors }
79
+ : {}),
71
80
  resumeToken,
72
81
  onActivity: input.onActivity,
82
+ forkEmitContext,
83
+ cloneEmitContext,
73
84
  }, input.operatorCatalog);
74
85
  if (!opened.ok)
75
86
  return opened;
@@ -132,6 +132,9 @@ export class StageProcessLauncher {
132
132
  if (input.sessionFilePath !== undefined) {
133
133
  args.push("--session-file", input.sessionFilePath);
134
134
  }
135
+ if (input.operatorCatalog?.cwd !== undefined) {
136
+ args.push("--operator-cwd", input.operatorCatalog.cwd);
137
+ }
135
138
  if (input.operatorCatalog?.agentDir !== undefined) {
136
139
  args.push("--operator-agent-dir", input.operatorCatalog.agentDir);
137
140
  }
@@ -34,9 +34,11 @@ export async function failStageAsInterrupted(options) {
34
34
  }
35
35
  }
36
36
  export async function syncRunStatusFromStages(store, runId) {
37
+ const meta = await store.readRunMeta(runId);
38
+ if (meta.status === "succeeded")
39
+ return;
37
40
  const run = await store.readRun(runId);
38
41
  const derived = deriveStatusFromStages(run.stages);
39
- const meta = await store.readRunMeta(runId);
40
42
  if ((meta.status ?? "created") !== derived) {
41
43
  await store.updateRunStatus(runId, derived);
42
44
  }
@@ -44,6 +44,7 @@ export type RunStageOptions = {
44
44
  operatorCatalog?: OperatorCatalog;
45
45
  completedEnvelopes?: Map<string, StageEnvelope>;
46
46
  skipGates?: boolean;
47
+ stageId?: string;
47
48
  };
48
49
  /**
49
50
  * Yield loop: next() until completed; on wait, coordinator enterWait (KTD6).
@@ -114,28 +114,29 @@ export async function runStageYieldLoop(options) {
114
114
  }
115
115
  export async function runStage(options) {
116
116
  const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, } = options;
117
+ const stageId = options.stageId ?? stage.id;
117
118
  const attemptOpt = attemptCtx?.eventOptions();
118
119
  const baseRoots = rootsOverride ??
119
- buildStageRoots(workspaceDir ?? store.getWorkspaceDir(runId), stage.id, checkoutRoot, attemptCtx);
120
+ buildStageRoots(workspaceDir ?? store.getWorkspaceDir(runId), stageId, checkoutRoot, attemptCtx);
120
121
  const roots = factoryCwd !== undefined
121
122
  ? withResolvedAuthPath(baseRoots, factoryCwd)
122
123
  : baseRoots;
123
124
  const attempt = attemptCtx?.attempt ?? 1;
124
- await ensureStageExecutionForAttempt(store, runId, stage.id, attempt);
125
- await store.ensureAttemptWorkspace(runId, stage.id, attempt);
125
+ await ensureStageExecutionForAttempt(store, runId, stageId, attempt);
126
+ await store.ensureAttemptWorkspace(runId, stageId, attempt);
126
127
  if (!skipStarted) {
127
128
  await appendLifecycleEventAndPatchExecution({
128
129
  store,
129
130
  runId,
130
- stageId: stage.id,
131
+ stageId,
131
132
  event: { event: "started" },
132
133
  attemptCtx,
133
134
  });
134
135
  }
135
- console.error(`Running stage ${stage.id} (${stage.model})...`);
136
+ console.error(`Running stage ${stageId} (${stage.model})...`);
136
137
  let activityChain = Promise.resolve();
137
138
  const enqueueActivity = (event) => {
138
- activityChain = activityChain.then(() => store.appendStageEvent(runId, stage.id, event, attemptOpt));
139
+ activityChain = activityChain.then(() => store.appendStageEvent(runId, stageId, event, attemptOpt));
139
140
  };
140
141
  let handle;
141
142
  if (existingHandle) {
@@ -147,12 +148,13 @@ export async function runStage(options) {
147
148
  store,
148
149
  runId,
149
150
  stage,
151
+ stageId,
150
152
  task,
151
153
  dag: dag ?? {
152
154
  nodes: [
153
- { id: stage.id, needs: null, ancestors: [], stageIndex: 0 },
155
+ { id: stageId, needs: null, ancestors: [], stageIndex: 0 },
154
156
  ],
155
- roots: [stage.id],
157
+ roots: [stageId],
156
158
  childrenOf: {},
157
159
  },
158
160
  checkoutRoot,
@@ -170,11 +172,11 @@ export async function runStage(options) {
170
172
  await appendLifecycleEventAndPatchExecution({
171
173
  store,
172
174
  runId,
173
- stageId: stage.id,
175
+ stageId,
174
176
  event: { event: "failed", reason: opened.reason },
175
177
  attemptCtx,
176
178
  });
177
- console.error(`Stage ${stage.id} failed: ${opened.reason}`);
179
+ console.error(`Stage ${stageId} failed: ${opened.reason}`);
178
180
  return { ok: false, reason: opened.reason };
179
181
  }
180
182
  handle = opened.handle;
@@ -184,7 +186,7 @@ export async function runStage(options) {
184
186
  outcome = await runStageYieldLoop({
185
187
  handle,
186
188
  runId,
187
- stageId: stage.id,
189
+ stageId,
188
190
  hitl,
189
191
  workerMode,
190
192
  store: workerMode ? store : undefined,
@@ -195,15 +197,15 @@ export async function runStage(options) {
195
197
  catch (err) {
196
198
  const reason = err instanceof Error ? err.message : String(err);
197
199
  await activityChain;
198
- hitl?.clearLiveWait(runId, stage.id);
200
+ hitl?.clearLiveWait(runId, stageId);
199
201
  await appendLifecycleEventAndPatchExecution({
200
202
  store,
201
203
  runId,
202
- stageId: stage.id,
204
+ stageId,
203
205
  event: { event: "failed", reason },
204
206
  attemptCtx,
205
207
  });
206
- console.error(`Stage ${stage.id} failed: ${reason}`);
208
+ console.error(`Stage ${stageId} failed: ${reason}`);
207
209
  await handle.close().catch(() => undefined);
208
210
  return { ok: false, reason };
209
211
  }
@@ -216,7 +218,7 @@ export async function runStage(options) {
216
218
  return await finalizeStageResult({
217
219
  store,
218
220
  runId,
219
- stageId: stage.id,
221
+ stageId,
220
222
  result: outcome,
221
223
  activityChain,
222
224
  attemptCtx,