stageflow 0.18.0 → 0.23.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 (116) hide show
  1. package/README.md +2 -1
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/activityObserver.d.ts +3 -0
  4. package/dist/agent/activityObserver.js +6 -1
  5. package/dist/agent/claudeAdapter.js +38 -0
  6. package/dist/agent/fakeAgent.js +3 -1
  7. package/dist/agent/piAdapter.js +40 -4
  8. package/dist/agent/port.d.ts +3 -1
  9. package/dist/agent/providerInspect.d.ts +19 -0
  10. package/dist/agent/providerInspect.js +35 -0
  11. package/dist/agent/stageTimeout.d.ts +9 -0
  12. package/dist/agent/stageTimeout.js +24 -0
  13. package/dist/agent/streamLogRedact.d.ts +2 -0
  14. package/dist/agent/streamLogRedact.js +13 -0
  15. package/dist/cli/artifactCommand.js +2 -2
  16. package/dist/cli/envelopeCommand.js +2 -2
  17. package/dist/cli/exportRunCommand.js +2 -2
  18. package/dist/cli/graphCommand.d.ts +15 -0
  19. package/dist/cli/graphCommand.js +79 -0
  20. package/dist/cli/graphRender.d.ts +2 -0
  21. package/dist/cli/graphRender.js +85 -0
  22. package/dist/cli/hostClient.d.ts +43 -0
  23. package/dist/cli/hostClient.js +182 -0
  24. package/dist/cli/providersCommand.js +5 -3
  25. package/dist/cli/runCommand.d.ts +6 -5
  26. package/dist/cli/runCommand.js +23 -31
  27. package/dist/cli/runOutput.d.ts +1 -1
  28. package/dist/cli/runsCommand.d.ts +4 -8
  29. package/dist/cli/runsCommand.js +65 -89
  30. package/dist/cli.js +13 -5
  31. package/dist/config/describePipeline.d.ts +26 -0
  32. package/dist/config/describePipeline.js +37 -0
  33. package/dist/config/loadPipeline.d.ts +13 -1
  34. package/dist/config/loadPipeline.js +34 -4
  35. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  36. package/dist/config/mergePipelineIncludes.js +75 -0
  37. package/dist/config/validateCatalog.d.ts +2 -2
  38. package/dist/config/validateCatalog.js +24 -17
  39. package/dist/mcp/catalogTools.js +146 -52
  40. package/dist/mcp/controlTools.js +94 -19
  41. package/dist/mcp/deps.d.ts +3 -0
  42. package/dist/mcp/projectMcpTools.d.ts +3 -0
  43. package/dist/mcp/projectMcpTools.js +25 -0
  44. package/dist/mcp/providerTools.d.ts +3 -0
  45. package/dist/mcp/providerTools.js +17 -0
  46. package/dist/mcp/readArtifact.d.ts +9 -0
  47. package/dist/mcp/readArtifact.js +11 -0
  48. package/dist/mcp/server.js +9 -1
  49. package/dist/mcp/tailStreamLog.d.ts +8 -0
  50. package/dist/mcp/tailStreamLog.js +28 -0
  51. package/dist/mcp/toolResults.d.ts +17 -0
  52. package/dist/mcp/toolResults.js +15 -0
  53. package/dist/mcp/tools.js +4 -0
  54. package/dist/mcp/waitRun.js +4 -4
  55. package/dist/mcp/waitingGates.d.ts +1 -0
  56. package/dist/mcp/waitingGates.js +3 -2
  57. package/dist/package-meta.d.ts +1 -1
  58. package/dist/package-meta.js +1 -1
  59. package/dist/projection/projectRun.d.ts +3 -0
  60. package/dist/projection/projectRun.js +7 -0
  61. package/dist/runstore/port.d.ts +3 -1
  62. package/dist/runstore/port.js +2 -0
  63. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
  64. package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
  65. package/dist/runstore/workspaceLayout.d.ts +1 -0
  66. package/dist/runstore/workspaceLayout.js +3 -0
  67. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  68. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  69. package/dist/runtime/feedbackLoopDecision.js +33 -1
  70. package/dist/runtime/forkGeneration.d.ts +4 -0
  71. package/dist/runtime/forkGeneration.js +16 -0
  72. package/dist/runtime/pipelineRunner.d.ts +3 -3
  73. package/dist/runtime/pipelineRunner.js +3 -1
  74. package/dist/runtime/pipelineScheduler.js +23 -1
  75. package/dist/runtime/reopenSkipped.d.ts +13 -0
  76. package/dist/runtime/reopenSkipped.js +56 -0
  77. package/dist/runtime/resumeReconstruct.js +2 -2
  78. package/dist/runtime/resumeTimedOut.d.ts +33 -0
  79. package/dist/runtime/resumeTimedOut.js +165 -0
  80. package/dist/runtime/runManager.d.ts +3 -1
  81. package/dist/runtime/runManager.js +110 -14
  82. package/dist/runtime/settingsFile.d.ts +2 -0
  83. package/dist/runtime/settingsFile.js +19 -0
  84. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  85. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  86. package/dist/runtime/stageRecovery.js +2 -2
  87. package/dist/runtime/stageRunner.js +6 -2
  88. package/dist/runtime/stageStreamLog.d.ts +17 -0
  89. package/dist/runtime/stageStreamLog.js +89 -0
  90. package/dist/runtime/stageWorker.js +2 -1
  91. package/dist/server/bootstrap.js +9 -2
  92. package/dist/server/ensureGlobalService.d.ts +35 -0
  93. package/dist/server/ensureGlobalService.js +138 -0
  94. package/dist/server/http.d.ts +20 -1
  95. package/dist/server/http.js +452 -386
  96. package/dist/server/mcpHost.js +7 -8
  97. package/dist/server/operatorResults.d.ts +10 -0
  98. package/dist/server/operatorResults.js +24 -0
  99. package/dist/server/providerRoutes.d.ts +0 -6
  100. package/dist/server/providerRoutes.js +10 -18
  101. package/dist/tools/askOperator.d.ts +1 -0
  102. package/dist/tools/askOperator.js +14 -11
  103. package/dist/tools/emitStageEnvelope.d.ts +1 -0
  104. package/dist/tools/emitStageEnvelope.js +3 -1
  105. package/dist/tools/prepareToolArguments.d.ts +4 -0
  106. package/dist/tools/prepareToolArguments.js +79 -0
  107. package/dist/types/pipeline.d.ts +8 -0
  108. package/dist/ui/assets/{index-DsgVjLUS.css → index-CSsDued1.css} +1 -1
  109. package/dist/ui/assets/index-Da5EHU6M.js +119 -0
  110. package/dist/ui/index.html +2 -2
  111. package/package.json +1 -1
  112. package/skills/stageflow/references/control-surface.md +2 -2
  113. package/skills/stageflow-run/SKILL.md +27 -9
  114. package/skills/stageflow-run/references/mcp-call.md +1 -1
  115. package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
  116. package/dist/ui/assets/index-DwKGaQvQ.js +0 -119
@@ -0,0 +1,56 @@
1
+ import { normalizeForkChoice } from "../envelope/forkChoice.js";
2
+ import { forkCohortMapsFromStore } from "./forkGeneration.js";
3
+ import { joinAllowsRun } from "./joinReadiness.js";
4
+ export function isUnchosenForkChild(dag, stageId, envelopes) {
5
+ for (const node of dag.nodes) {
6
+ const envelope = envelopes.get(node.id);
7
+ if (envelope?.fork_choice === undefined || envelope.fork_choice === null) {
8
+ continue;
9
+ }
10
+ const children = dag.childrenOf[node.id] ?? [];
11
+ if (!children.includes(stageId))
12
+ continue;
13
+ const chosen = normalizeForkChoice(envelope.fork_choice, "stored");
14
+ if (!chosen.has(stageId))
15
+ return true;
16
+ }
17
+ return false;
18
+ }
19
+ export function eventsShowStarted(events) {
20
+ return events.some((event) => event.event === "started" || event.event === "resumed");
21
+ }
22
+ export function shouldReopenSkippedStage(dag, stageId, states, envelopes, hasStarted, options) {
23
+ if (hasStarted)
24
+ return false;
25
+ if (options?.supersededCloneIds?.has(stageId))
26
+ return false;
27
+ if (states.get(stageId) !== "skipped")
28
+ return false;
29
+ if (isUnchosenForkChild(dag, stageId, envelopes))
30
+ return false;
31
+ const hypothetical = new Map(states);
32
+ hypothetical.set(stageId, "pending");
33
+ return joinAllowsRun(dag, stageId, hypothetical, envelopes);
34
+ }
35
+ export async function persistReopenedStages(store, runId, stageIds, states) {
36
+ for (const id of stageIds) {
37
+ if (states.get(id) !== "skipped")
38
+ continue;
39
+ await store.appendStageEvent(runId, id, { event: "reopened" });
40
+ states.set(id, "pending");
41
+ }
42
+ }
43
+ export async function reopenRunnableSkippedStages(store, runId, dag, states, envelopes) {
44
+ const { supersededCloneIds } = await forkCohortMapsFromStore(store, runId);
45
+ const ids = [];
46
+ for (const [id, state] of states) {
47
+ if (state !== "skipped")
48
+ continue;
49
+ const events = await store.listStageEvents(runId, id);
50
+ if (!shouldReopenSkippedStage(dag, id, states, envelopes, eventsShowStarted(events), { supersededCloneIds })) {
51
+ continue;
52
+ }
53
+ ids.push(id);
54
+ }
55
+ await persistReopenedStages(store, runId, ids, states);
56
+ }
@@ -33,12 +33,12 @@ export async function loadRunContext(store, runId, cwd) {
33
33
  }
34
34
  export async function reconstructAndContinue(ctx) {
35
35
  const { runId, stageId, opaqueAnswer, agent, store, cwd } = ctx;
36
- const factoryCwd = ctx.factoryCwd ?? cwd;
37
36
  try {
38
37
  const latestExecution = await store.getLatestStageExecution(runId, stageId);
39
38
  const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
40
39
  const attemptOpt = attemptCtx.eventOptions();
41
40
  const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
41
+ const factoryCwd = meta.project_root ?? ctx.factoryCwd ?? cwd;
42
42
  const definitionId = definitionIdForInstance(meta.pipeline_dag, stageId);
43
43
  const stageIndex = loaded.stages.findIndex((s) => s.id === definitionId);
44
44
  if (stageIndex < 0) {
@@ -127,7 +127,7 @@ export async function reconstructAndContinue(ctx) {
127
127
  run: { runId, workspaceDir },
128
128
  agent,
129
129
  store,
130
- cwd,
130
+ cwd: factoryCwd,
131
131
  projectRoot: factoryCwd,
132
132
  checkoutRoot,
133
133
  hitl: ctx.hitl,
@@ -0,0 +1,33 @@
1
+ import type { AgentPort } from "../agent/port.js";
2
+ import type { RunStore } from "../runstore/port.js";
3
+ import type { StageExecutionMode } from "./stageConcurrency.js";
4
+ import type { StageProcessLauncher } from "./stageProcessLauncher.js";
5
+ import { StageHitlController } from "./stageHitl.js";
6
+ import { type OperatorCatalog } from "./stageAttemptBootstrap.js";
7
+ export type ResumeTimedOutContext = {
8
+ runId: string;
9
+ stageId: string;
10
+ agent: AgentPort;
11
+ store: RunStore;
12
+ hitl: StageHitlController;
13
+ executionMode: StageExecutionMode;
14
+ stageProcessLauncher?: StageProcessLauncher;
15
+ cwd: string;
16
+ factoryCwd?: string;
17
+ maxActiveStagesPerRun: number;
18
+ operatorCatalog?: OperatorCatalog;
19
+ };
20
+ export declare function assertTimedOutStageEligible(status: string, events: ReadonlyArray<{
21
+ event: string;
22
+ reason?: string;
23
+ }>): {
24
+ ok: true;
25
+ } | {
26
+ ok: false;
27
+ reason: string;
28
+ status: 409;
29
+ };
30
+ export declare function reconstructTimedOutAndContinue(ctx: ResumeTimedOutContext): Promise<{
31
+ ok: boolean;
32
+ reason?: string;
33
+ }>;
@@ -0,0 +1,165 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fakeHitlResumePath } from "../agent/fakeAgent.js";
3
+ import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
4
+ import { attemptSessionPath } from "../runstore/workspaceLayout.js";
5
+ import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
6
+ import { resumeRun } from "./pipelineScheduler.js";
7
+ import { loadRunContext } from "./resumeReconstruct.js";
8
+ import { isRunStageWaiting, runStage } from "./stageRunner.js";
9
+ import { attemptContext } from "./stageAttemptContext.js";
10
+ import { openStageAttempt, } from "./stageAttemptBootstrap.js";
11
+ import { buildStageRoots, withResolvedAuthPath } from "./stageRoots.js";
12
+ import { isStageTimeoutReason, lastFailedReason, } from "../agent/stageTimeout.js";
13
+ import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
14
+ export function assertTimedOutStageEligible(status, events) {
15
+ if (status !== "failed") {
16
+ return {
17
+ ok: false,
18
+ reason: `Stage is not timed out (status=${status})`,
19
+ status: 409,
20
+ };
21
+ }
22
+ if (!isStageTimeoutReason(lastFailedReason(events))) {
23
+ return {
24
+ ok: false,
25
+ reason: "Stage did not fail due to timeout; use retry to start a new attempt",
26
+ status: 409,
27
+ };
28
+ }
29
+ return { ok: true };
30
+ }
31
+ export async function reconstructTimedOutAndContinue(ctx) {
32
+ const { runId, stageId, agent, store, cwd } = ctx;
33
+ const factoryCwd = ctx.factoryCwd ?? cwd;
34
+ try {
35
+ const latestExecution = await store.getLatestStageExecution(runId, stageId);
36
+ const attempt = latestExecution?.attempt ?? 1;
37
+ const attemptCtx = attemptContext(attempt);
38
+ const attemptOpt = attemptCtx.eventOptions();
39
+ const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
40
+ const definitionId = definitionIdForInstance(meta.pipeline_dag, stageId);
41
+ const stageIndex = loaded.stages.findIndex((s) => s.id === definitionId);
42
+ if (stageIndex < 0) {
43
+ const reason = `Stage ${stageId} not in pipeline ${meta.pipeline_id}`;
44
+ await store.appendStageEvent(runId, stageId, { event: "failed", reason }, attemptOpt);
45
+ await store.updateRunStatus(runId, "failed");
46
+ return { ok: false, reason };
47
+ }
48
+ const stage = loaded.stages[stageIndex];
49
+ const dag = meta.pipeline_dag ?? loaded.dag;
50
+ const workspaceDir = store.getWorkspaceDir(runId);
51
+ const checkoutRoot = meta.checkout_root;
52
+ const roots = withResolvedAuthPath(buildStageRoots(workspaceDir, stageId, checkoutRoot, attemptCtx), factoryCwd);
53
+ const sessionFile = attemptSessionPath(workspaceDir, stageId, attempt);
54
+ const fakeResume = fakeHitlResumePath(roots, stageId);
55
+ if (!existsSync(sessionFile) && !existsSync(fakeResume)) {
56
+ return {
57
+ ok: false,
58
+ reason: "missing session to resume; use retry to start a new attempt",
59
+ };
60
+ }
61
+ const resumed = { event: "resumed" };
62
+ await store.appendStageEvent(runId, stageId, resumed, attemptOpt);
63
+ try {
64
+ const execution = await store.getStageExecution(runId, stageId, attempt);
65
+ const patch = deriveExecutionPatchFromEvent(resumed, execution);
66
+ if (Object.keys(patch).length > 0) {
67
+ await store.updateStageExecution(runId, stageId, attempt, patch);
68
+ }
69
+ }
70
+ catch {
71
+ // no execution row yet
72
+ }
73
+ await store.updateRunStatus(runId, "running");
74
+ const opened = await openStageAttempt({
75
+ agent,
76
+ store,
77
+ runId,
78
+ stage,
79
+ stageId,
80
+ task,
81
+ dag,
82
+ checkoutRoot,
83
+ workspaceDir,
84
+ factoryCwd,
85
+ attemptCtx,
86
+ operatorCatalog: ctx.operatorCatalog,
87
+ sessionMode: "timeout_resume",
88
+ resumeToken: sessionFile,
89
+ onActivity: (event) => {
90
+ void store.appendStageEvent(runId, stageId, event, attemptOpt);
91
+ },
92
+ });
93
+ if (!opened.ok) {
94
+ await store.appendStageEvent(runId, stageId, { event: "failed", reason: opened.reason }, attemptOpt);
95
+ await store.updateRunStatus(runId, "failed");
96
+ return { ok: false, reason: opened.reason };
97
+ }
98
+ const result = await runStage({
99
+ agent,
100
+ store,
101
+ runId,
102
+ stage,
103
+ stageId,
104
+ task,
105
+ dag,
106
+ checkoutRoot,
107
+ workspaceDir,
108
+ hitl: ctx.hitl,
109
+ skipStarted: true,
110
+ existingHandle: opened.handle,
111
+ attemptCtx,
112
+ factoryCwd,
113
+ operatorCatalog: ctx.operatorCatalog,
114
+ sessionMode: "timeout_resume",
115
+ resumeToken: sessionFile,
116
+ });
117
+ if (isRunStageWaiting(result)) {
118
+ await store.updateRunStatus(runId, "failed");
119
+ return {
120
+ ok: false,
121
+ reason: WAIT_WITHOUT_WORKER_DISPATCH,
122
+ };
123
+ }
124
+ if (!result.ok) {
125
+ await store.updateRunStatus(runId, "failed");
126
+ return { ok: false, reason: result.reason };
127
+ }
128
+ const rest = await resumeRun({
129
+ prepared: {
130
+ task,
131
+ loaded,
132
+ run: { runId, workspaceDir },
133
+ agent,
134
+ store,
135
+ cwd,
136
+ projectRoot: factoryCwd,
137
+ checkoutRoot,
138
+ hitl: ctx.hitl,
139
+ operatorCatalog: ctx.operatorCatalog,
140
+ },
141
+ maxActiveStagesPerRun: ctx.maxActiveStagesPerRun,
142
+ resumeFromStageId: stageId,
143
+ initialPrior: result.envelope ?? null,
144
+ executionMode: ctx.executionMode,
145
+ stageProcessLauncher: ctx.stageProcessLauncher,
146
+ });
147
+ if (rest.outcome === "failed") {
148
+ return { ok: false, reason: rest.reason };
149
+ }
150
+ return { ok: true };
151
+ }
152
+ catch (err) {
153
+ const reason = err instanceof Error ? err.message : `timeout resume failed: ${String(err)}`;
154
+ try {
155
+ const latestExecution = await store.getLatestStageExecution(runId, stageId);
156
+ const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
157
+ await store.appendStageEvent(runId, stageId, { event: "failed", reason }, attemptCtx.eventOptions());
158
+ await store.updateRunStatus(runId, "failed");
159
+ }
160
+ catch {
161
+ // ignore secondary failures
162
+ }
163
+ return { ok: false, reason };
164
+ }
165
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AgentPort, OpaqueAnswer } from "../agent/port.js";
2
+ import type { InlinePipelineDefinition } from "../types/pipeline.js";
2
3
  import { type RunStore } from "../runstore/port.js";
3
4
  import type { TaskFile } from "../types/task.js";
4
5
  import { type PipelineRunResult } from "./pipelineRunner.js";
@@ -125,7 +126,7 @@ export declare class RunManager {
125
126
  }>>;
126
127
  abandonStage(runId: string, stageId: string): Promise<AbandonStageResult>;
127
128
  startRun(input: StartTaskInput & {
128
- pipeline: string;
129
+ pipeline: string | InlinePipelineDefinition;
129
130
  task?: string | TaskFile;
130
131
  checkoutOverride?: string;
131
132
  skipGates?: boolean;
@@ -135,6 +136,7 @@ export declare class RunManager {
135
136
  }): Promise<StartRunResult>;
136
137
  rerun(runId: string): Promise<StartRunResult>;
137
138
  retryStage(runId: string, stageId: string): Promise<RetryStageResult>;
139
+ resumeTimedOutStage(runId: string, stageId: string): Promise<RetryStageResult>;
138
140
  recoverManualStage(runId: string, stageId: string, guidance?: string): Promise<RetryStageResult>;
139
141
  recoverManualStageUntilStop(runId: string, stageId: string, guidance?: string): Promise<{
140
142
  ok: true;
@@ -1,8 +1,10 @@
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 { findProjectRoot } from "../project/findProjectRoot.js";
4
5
  import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
5
6
  import { loadRunContext } from "./resumeReconstruct.js";
7
+ import { assertTimedOutStageEligible, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
6
8
  import { loadTaskFromYaml } from "../config/loadTask.js";
7
9
  import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
8
10
  import { PipelineValidationError, startPipeline, } from "./pipelineRunner.js";
@@ -11,7 +13,7 @@ import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
11
13
  import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
12
14
  import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
13
15
  import { StageProcessLauncher } from "./stageProcessLauncher.js";
14
- import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, projectSettingsContext, readMaxConcurrentFromContext, writeMaxConcurrentToContext, } from "./settingsFile.js";
16
+ import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
15
17
  import { StageHitlController, waitKey, } from "./stageHitl.js";
16
18
  import { resolveAndValidateCheckout, } from "./stageRoots.js";
17
19
  import { orchestrateAnswerResume } from "./answerResume.js";
@@ -93,10 +95,9 @@ export class RunManager {
93
95
  this.cwd = options.cwd ?? process.cwd();
94
96
  this.projectRoot = options.projectRoot ?? this.cwd;
95
97
  this.isGitProject = options.isGitProject ?? false;
96
- const settingsCtx = projectSettingsContext(this.cwd, this.projectRoot, this.isGitProject);
97
98
  this.maxConcurrent =
98
99
  options.maxConcurrent ??
99
- readMaxConcurrentFromContext(settingsCtx) ??
100
+ readMaxConcurrentFromGlobal() ??
100
101
  parseMaxConcurrent(process.env.STAGEFLOW_MAX_CONCURRENT_RUNS);
101
102
  this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
102
103
  this.executionMode = readStageExecutionMode(process.env, options.executionMode);
@@ -118,7 +119,7 @@ export class RunManager {
118
119
  throw new Error(INVALID_SLOT_COUNT_MESSAGE);
119
120
  }
120
121
  this.maxConcurrent = parsed;
121
- writeMaxConcurrentToContext(projectSettingsContext(this.cwd, this.projectRoot, this.isGitProject), parsed);
122
+ writeMaxConcurrentToGlobal(parsed);
122
123
  return this.getHealth();
123
124
  }
124
125
  getActiveCount() {
@@ -349,6 +350,11 @@ export class RunManager {
349
350
  }
350
351
  async startRun(input) {
351
352
  const cwd = this.options.cwd ?? process.cwd();
353
+ // An inline pipeline has no filesystem anchor to derive a project root from.
354
+ const derivedProjectRoot = typeof input.pipeline === "string"
355
+ ? (findProjectRoot(path.dirname(path.resolve(cwd, input.pipeline))) ??
356
+ this.projectRoot)
357
+ : this.projectRoot;
352
358
  let resolved;
353
359
  try {
354
360
  resolved = resolveStartTaskInput(input, cwd);
@@ -383,7 +389,7 @@ export class RunManager {
383
389
  gitSha: input.gitSha,
384
390
  ciPrUrl: input.ciPrUrl,
385
391
  ciJobUrl: input.ciJobUrl,
386
- }, undefined, resolved.kind === "path" ? resolved.taskPath : undefined);
392
+ }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined);
387
393
  }
388
394
  async rerun(runId) {
389
395
  const cwd = this.options.cwd ?? process.cwd();
@@ -417,6 +423,82 @@ export class RunManager {
417
423
  async retryStage(runId, stageId) {
418
424
  return this.retryStageInternal(runId, stageId, true);
419
425
  }
426
+ async resumeTimedOutStage(runId, stageId) {
427
+ const resumeKey = waitKey(runId, stageId);
428
+ if (this.resumeInFlight.has(resumeKey) || this.retryInFlight.has(resumeKey)) {
429
+ return {
430
+ ok: false,
431
+ reason: `Resume already in progress for run ${runId} stage ${stageId}`,
432
+ status: 409,
433
+ };
434
+ }
435
+ this.resumeInFlight.add(resumeKey);
436
+ let detail;
437
+ try {
438
+ detail = await this.options.store.readRun(runId);
439
+ }
440
+ catch {
441
+ this.resumeInFlight.delete(resumeKey);
442
+ return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
443
+ }
444
+ const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
445
+ if (!stageSnap) {
446
+ this.resumeInFlight.delete(resumeKey);
447
+ return { ok: false, reason: `Stage not found: ${stageId}`, status: 404 };
448
+ }
449
+ const eligibility = assertTimedOutStageEligible(stageSnap.status, stageSnap.events);
450
+ if (!eligibility.ok) {
451
+ this.resumeInFlight.delete(resumeKey);
452
+ return eligibility;
453
+ }
454
+ const latest = await this.options.store.getLatestStageExecution(runId, stageId);
455
+ const attemptIndex = latest?.attempt ?? 1;
456
+ const wasActive = this.active.has(runId);
457
+ const tracked = await this.ensureResumeTracked(runId);
458
+ if (!tracked.ok) {
459
+ this.resumeInFlight.delete(resumeKey);
460
+ return { ok: false, reason: tracked.reason, status: 409 };
461
+ }
462
+ const insertedForResume = !wasActive;
463
+ try {
464
+ const runProjectRoot = detail.project_root ?? this.projectRoot;
465
+ const done = reconstructTimedOutAndContinue({
466
+ runId,
467
+ stageId,
468
+ agent: this.options.agent,
469
+ store: this.options.store,
470
+ hitl: this.hitl,
471
+ executionMode: this.executionMode,
472
+ cwd: runProjectRoot,
473
+ maxActiveStagesPerRun: this.maxActiveStagesPerRun,
474
+ factoryCwd: runProjectRoot,
475
+ ...(this.stageProcessLauncher !== undefined
476
+ ? { stageProcessLauncher: this.stageProcessLauncher }
477
+ : {}),
478
+ ...(this.options.operatorCatalog !== undefined
479
+ ? { operatorCatalog: this.options.operatorCatalog }
480
+ : {}),
481
+ });
482
+ this.registerResumeUntrack(runId, done);
483
+ const outcome = await done;
484
+ if (!outcome.ok) {
485
+ if (insertedForResume) {
486
+ this.removeActiveEntry(runId, false);
487
+ }
488
+ const missingSession = outcome.reason !== undefined &&
489
+ outcome.reason.startsWith("missing session to resume");
490
+ return {
491
+ ok: false,
492
+ reason: outcome.reason ?? "timeout resume failed",
493
+ status: missingSession ? 409 : 500,
494
+ };
495
+ }
496
+ return { ok: true, runId, stageId, attemptIndex };
497
+ }
498
+ finally {
499
+ this.resumeInFlight.delete(resumeKey);
500
+ }
501
+ }
420
502
  async recoverManualStage(runId, stageId, guidance) {
421
503
  return this.recoverManualStageInternal(runId, stageId, guidance, true);
422
504
  }
@@ -509,6 +591,7 @@ export class RunManager {
509
591
  };
510
592
  }
511
593
  try {
594
+ let meta;
512
595
  let orchestrationConflict = this.active.has(runId) &&
513
596
  !this.attachedWaiting.has(waitKey(runId, stageId));
514
597
  if (!this.retryCoordinator.isActive(runId)) {
@@ -531,7 +614,7 @@ export class RunManager {
531
614
  let detail;
532
615
  try {
533
616
  detail = await this.options.store.readRun(runId);
534
- await this.options.store.readRunMeta(runId);
617
+ meta = await this.options.store.readRunMeta(runId);
535
618
  }
536
619
  catch {
537
620
  this.retryInFlight.delete(retryKey);
@@ -550,12 +633,21 @@ export class RunManager {
550
633
  }
551
634
  }
552
635
  }
636
+ if (meta === undefined) {
637
+ try {
638
+ meta = await this.options.store.readRunMeta(runId);
639
+ }
640
+ catch {
641
+ this.retryInFlight.delete(retryKey);
642
+ return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
643
+ }
644
+ }
553
645
  const result = await this.retryCoordinator.retryStage({
554
646
  runId,
555
647
  stageId,
556
648
  store: this.options.store,
557
649
  agent: this.options.agent,
558
- cwd: this.cwd,
650
+ cwd: meta.project_root ?? this.projectRoot,
559
651
  operatorCatalog: this.options.operatorCatalog,
560
652
  maxActiveStagesPerRun: this.maxActiveStagesPerRun,
561
653
  executionMode: this.executionMode,
@@ -904,6 +996,7 @@ export class RunManager {
904
996
  };
905
997
  }
906
998
  const { meta, task, loaded } = await loadRunContext(this.options.store, runId, this.cwd);
999
+ const runProjectRoot = meta.project_root ?? this.projectRoot;
907
1000
  const done = runPipelineDag({
908
1001
  prepared: {
909
1002
  task,
@@ -914,8 +1007,8 @@ export class RunManager {
914
1007
  },
915
1008
  agent: this.options.agent,
916
1009
  store: this.options.store,
917
- cwd: this.cwd,
918
- projectRoot: this.projectRoot,
1010
+ cwd: runProjectRoot,
1011
+ projectRoot: runProjectRoot,
919
1012
  checkoutRoot: meta.checkout_root,
920
1013
  hitl: this.hitl,
921
1014
  operatorCatalog: this.options.operatorCatalog,
@@ -965,10 +1058,12 @@ export class RunManager {
965
1058
  const sessionFilePath = resumeSessionFilePath(workspaceDir, stageId, attempt);
966
1059
  const eventOptions = { attempt };
967
1060
  try {
1061
+ const runMeta = await store.readRunMeta(runId);
1062
+ const runProjectRoot = runMeta.project_root ?? this.projectRoot;
968
1063
  const launchResult = await launcher.launch({
969
1064
  runId,
970
1065
  stageId,
971
- rootDir: this.projectRoot,
1066
+ rootDir: runProjectRoot,
972
1067
  mode: "resume",
973
1068
  resumeAnswer: opaqueAnswer,
974
1069
  attempt,
@@ -1003,8 +1098,8 @@ export class RunManager {
1003
1098
  run: { runId, workspaceDir: store.getWorkspaceDir(runId) },
1004
1099
  agent: this.options.agent,
1005
1100
  store,
1006
- cwd: this.cwd,
1007
- projectRoot: this.projectRoot,
1101
+ cwd: meta.project_root ?? this.projectRoot,
1102
+ projectRoot: meta.project_root ?? this.projectRoot,
1008
1103
  checkoutRoot: meta.checkout_root,
1009
1104
  hitl: this.hitl,
1010
1105
  operatorCatalog: this.options.operatorCatalog,
@@ -1201,6 +1296,7 @@ export class RunManager {
1201
1296
  return false;
1202
1297
  }
1203
1298
  try {
1299
+ const runProjectRoot = meta.project_root ?? this.projectRoot;
1204
1300
  const { meta: loadedMeta, task, loaded, workspaceDir } = await loadRunContext(this.options.store, runId, this.cwd);
1205
1301
  const promise = runPipelineDag({
1206
1302
  prepared: {
@@ -1209,8 +1305,8 @@ export class RunManager {
1209
1305
  run: { runId, workspaceDir },
1210
1306
  agent: this.options.agent,
1211
1307
  store: this.options.store,
1212
- cwd: this.cwd,
1213
- projectRoot: this.projectRoot,
1308
+ cwd: runProjectRoot,
1309
+ projectRoot: runProjectRoot,
1214
1310
  checkoutRoot: loadedMeta.checkout_root,
1215
1311
  hitl: this.hitl,
1216
1312
  operatorCatalog: this.options.operatorCatalog,
@@ -23,6 +23,8 @@ export declare function readMaxConcurrentFromContext(ctx: ProjectContext): numbe
23
23
  export declare function readMaxConcurrentFromFile(cwd: string): number | undefined;
24
24
  export declare function writeMaxConcurrentToContext(ctx: ProjectContext, maxConcurrent: number): void;
25
25
  export declare function writeMaxConcurrentToFile(cwd: string, maxConcurrent: number): void;
26
+ export declare function readMaxConcurrentFromGlobal(): number | undefined;
27
+ export declare function writeMaxConcurrentToGlobal(maxConcurrent: number): void;
26
28
  export declare function readCredentialSourceFromFile(cwd: string): CredentialSource | undefined;
27
29
  export declare function writeCredentialSourceToContext(ctx: ProjectContext, credentialSource: CredentialSource): void;
28
30
  export declare function writeCredentialSourceToFile(cwd: string, credentialSource: CredentialSource): void;
@@ -150,6 +150,25 @@ export function writeMaxConcurrentToContext(ctx, maxConcurrent) {
150
150
  export function writeMaxConcurrentToFile(cwd, maxConcurrent) {
151
151
  writeFactorySettings(cwd, { maxConcurrent });
152
152
  }
153
+ export function readMaxConcurrentFromGlobal() {
154
+ return parseFactorySettingsFromRaw(readRawSettingsFromFile(globalSettingsFilePath())).maxConcurrent;
155
+ }
156
+ export function writeMaxConcurrentToGlobal(maxConcurrent) {
157
+ if (parseSlotCount(maxConcurrent) === undefined) {
158
+ throw new Error(INVALID_SLOT_COUNT_MESSAGE);
159
+ }
160
+ const current = parseFactorySettingsFromRaw(readRawSettingsFromFile(globalSettingsFilePath()));
161
+ const next = { ...current, maxConcurrent };
162
+ const out = {};
163
+ if (next.maxConcurrent !== undefined) {
164
+ out.maxConcurrent = next.maxConcurrent;
165
+ }
166
+ if (next.credentialSource !== undefined) {
167
+ out.credentialSource = next.credentialSource;
168
+ }
169
+ mkdirSync(path.dirname(globalSettingsFilePath()), { recursive: true });
170
+ writeFileSync(globalSettingsFilePath(), `${JSON.stringify(out, null, 2)}\n`, "utf8");
171
+ }
153
172
  export function readCredentialSourceFromFile(cwd) {
154
173
  return readFactorySettings(cwd).credentialSource;
155
174
  }
@@ -1,4 +1,5 @@
1
1
  import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunInput, StageSessionMode } from "../agent/port.js";
2
+ import { type StageStreamLogWriter } from "./stageStreamLog.js";
2
3
  import type { RunStore } from "../runstore/port.js";
3
4
  import type { StageEnvelope } from "../types/envelope.js";
4
5
  import type { ResolvedPipelineDag } from "../types/pipeline.js";
@@ -23,6 +24,8 @@ export type StageAttemptOpenInput = {
23
24
  attemptCtx?: StageAttemptContext;
24
25
  operatorCatalog?: OperatorCatalog;
25
26
  onActivity?: StageRunInput["onActivity"];
27
+ /** Test seam; defaults to the real createStageStreamLogWriter. */
28
+ streamLogWriterFactory?: (streamLogPath: string) => StageStreamLogWriter;
26
29
  roots?: StageRoots;
27
30
  resumeToken?: string;
28
31
  sessionMode?: StageSessionMode;
@@ -1,7 +1,8 @@
1
1
  import { resolveSkillByName } from "../config/listSkills.js";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { MCP_CATALOG_FILENAME, STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV, StageMcpError, resolveStageMcpServers, stampStagePromptArtifactsDir, } from "../config/resolveStageMcpServers.js";
4
- import { attemptArtifactsDir } from "../runstore/workspaceLayout.js";
4
+ import { attemptArtifactsDir, attemptStreamLogPath } from "../runstore/workspaceLayout.js";
5
+ import { createStageStreamLogWriter, } from "./stageStreamLog.js";
5
6
  import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
6
7
  import { createAttemptQaTrailReader } from "../hitl/qaTrail.js";
7
8
  import { assertPriorInputPayload, payloadInstanceMismatch, } from "../envelope/payloadSchema.js";
@@ -199,6 +200,7 @@ export async function openStageAttempt(input) {
199
200
  return priorInput;
200
201
  const roots = resolveAttemptRoots(input, stageId);
201
202
  const attempt = input.attemptCtx?.attempt ?? 1;
203
+ const streamWriter = (input.streamLogWriterFactory ?? createStageStreamLogWriter)(attemptStreamLogPath(input.workspaceDir, stageId, attempt));
202
204
  const resumeToken = input.resumeToken ??
203
205
  resumeSessionFilePath(input.workspaceDir, stageId, attempt);
204
206
  const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
@@ -231,7 +233,11 @@ export async function openStageAttempt(input) {
231
233
  : {}),
232
234
  resumeToken,
233
235
  ...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
234
- onActivity: input.onActivity,
236
+ onActivity: (event) => {
237
+ input.onActivity?.(event);
238
+ void streamWriter.flush();
239
+ },
240
+ onAssistantTextDelta: streamWriter.onDelta,
235
241
  forkEmitContext,
236
242
  cloneEmitContext,
237
243
  ...(feedbackLoopEmitContext !== undefined ? { feedbackLoopEmitContext } : {}),
@@ -35,10 +35,10 @@ export async function failStageAsInterrupted(options) {
35
35
  }
36
36
  export async function syncRunStatusFromStages(store, runId) {
37
37
  const meta = await store.readRunMeta(runId);
38
- if (meta.status === "succeeded")
39
- return;
40
38
  const run = await store.readRun(runId);
41
39
  const derived = deriveStatusFromStages(run.stages, meta.pipeline_dag);
40
+ if (meta.status === "succeeded" && derived !== "running")
41
+ return;
42
42
  if ((meta.status ?? "created") !== derived) {
43
43
  await store.updateRunStatus(runId, derived);
44
44
  }
@@ -3,6 +3,7 @@ import { buildStageRoots, withResolvedAuthPath, } from "./stageRoots.js";
3
3
  import { createDefaultHitlQaHooks, recordWaitEnter, } from "./stageHitl.js";
4
4
  import { openStageAttempt, } from "./stageAttemptBootstrap.js";
5
5
  import { createVerifiedStageExecution } from "./verifiedStageExecution.js";
6
+ import { isStageTimeoutReason } from "../agent/stageTimeout.js";
6
7
  export function isRunStageWaiting(outcome) {
7
8
  return "waiting" in outcome && outcome.waiting === true;
8
9
  }
@@ -220,7 +221,9 @@ export async function runStage(options) {
220
221
  attemptCtx,
221
222
  });
222
223
  console.error(`Stage ${stageId} failed: ${reason}`);
223
- await handle.close().catch(() => undefined);
224
+ await handle
225
+ .close(isStageTimeoutReason(reason) ? { park: true } : undefined)
226
+ .catch(() => undefined);
224
227
  return { ok: false, reason };
225
228
  }
226
229
  if (isRunStageWaiting(outcome)) {
@@ -251,6 +254,7 @@ export async function runStage(options) {
251
254
  });
252
255
  }
253
256
  finally {
254
- await handle.close().catch(() => undefined);
257
+ const timedOut = isStageTimeoutReason(outcome.ok ? undefined : outcome.reason);
258
+ await handle.close(timedOut ? { park: true } : undefined).catch(() => undefined);
255
259
  }
256
260
  }